Python: [BREAKING] Standardize model selection on model (#4999)

* Refactor Anthropic model option and provider clients

Rename the Anthropic client model option from model_id to model, add provider-specific Anthropic wrappers for Foundry, Bedrock, and Vertex, and expose them through the Anthropic, Foundry, Amazon, and Google namespaces. Update core option handling, docs, samples, and tests accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Anthropic skills sample typing

Cast the Anthropic beta client to Any in the skills sample so the pre-commit sample pyright check no longer fails on beta skills and files endpoints that are not exposed by the current SDK stubs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* undo sample mypy

* Retry CI after transient external failures

Retrigger PR validation after an unrelated Copilot review workflow SAML failure and a transient external tau2 git fetch failure in the Windows Python test setup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback on model option merging

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Anthropic compatibility review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* moved all to `model`

* fixes for azure ai search

* Python: standardize remaining sample env var names

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix foundry-local pyright compatibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated env vars in cicd

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-04-01 21:00:18 +02:00
committed by GitHub
Unverified
parent 95550dd0dc
commit 6acab3d1d6
184 changed files with 1749 additions and 1025 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ OPENAI_CHAT_MODEL=""
OPENAI_RESPONSES_MODEL=""
# Azure OpenAI
AZURE_OPENAI_ENDPOINT=""
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=""
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=""
AZURE_OPENAI_CHAT_MODEL=""
AZURE_OPENAI_RESPONSES_MODEL=""
# Mem0
MEM0_API_KEY=""
# Copilot Studio
+4 -4
View File
@@ -480,7 +480,7 @@ A more complete example with keyword arguments and code samples:
```python
def create_client(
model_id: str | None = None,
model: str | None = None,
*,
timeout: float | None = None,
env_file_path: str | None = None,
@@ -489,7 +489,7 @@ def create_client(
"""Create a new client with the specified configuration.
Args:
model_id: The model ID to use. If not provided,
model: The model ID to use. If not provided,
it will be loaded from settings.
Keyword Args:
@@ -501,14 +501,14 @@ def create_client(
A configured client instance.
Raises:
ValueError: If the model_id is invalid.
ValueError: If the model is invalid.
Examples:
.. code-block:: python
# Create a client with default settings:
client = create_client(model_id="gpt-4o")
client = create_client(model="gpt-4o")
# Or load from environment:
client = create_client(env_file_path=".env")
+1 -1
View File
@@ -51,7 +51,7 @@ OPENAI_MODEL=...
...
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_ENDPOINT=...
AZURE_OPENAI_DEPLOYMENT_NAME=...
AZURE_OPENAI_MODEL=...
...
FOUNDRY_PROJECT_ENDPOINT=...
FOUNDRY_MODEL=...
@@ -108,7 +108,7 @@ class AGUIChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], tota
Keys:
# Inherited from ChatOptions (forwarded to remote server):
model_id: The model identifier (forwarded as-is to server).
model: The model identifier (forwarded as-is to server).
temperature: Sampling temperature.
top_p: Nucleus sampling parameter.
max_tokens: Maximum tokens to generate.
@@ -191,13 +191,13 @@ from fastapi import FastAPI
# Read required configuration
endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
deployment_name = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME")
model = os.environ.get("AZURE_OPENAI_MODEL")
api_key = os.environ.get("AZURE_OPENAI_API_KEY")
if not endpoint:
raise ValueError("AZURE_OPENAI_ENDPOINT environment variable is required")
if not deployment_name:
raise ValueError("AZURE_OPENAI_DEPLOYMENT_NAME environment variable is required")
if not model:
raise ValueError("AZURE_OPENAI_MODEL environment variable is required")
if not api_key:
raise ValueError("AZURE_OPENAI_API_KEY environment variable is required")
@@ -207,7 +207,7 @@ agent = Agent(
instructions="You are a helpful assistant.",
client=OpenAIChatCompletionClient(
azure_endpoint=endpoint,
model=deployment_name,
model=model,
api_key=api_key,
),
)
@@ -230,7 +230,7 @@ if __name__ == "__main__":
- **`Agent`**: The agent that will handle incoming requests
- **FastAPI Integration**: Uses FastAPI's native async support for streaming responses
- **Instructions**: The agent is created with default instructions, which can be overridden by client messages
- **Configuration**: `OpenAIChatCompletionClient` can read from environment variables (`AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_DEPLOYMENT_NAME`, `AZURE_OPENAI_API_KEY`) or accept parameters directly
- **Configuration**: `OpenAIChatCompletionClient` can read from environment variables (`AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_MODEL`, `AZURE_OPENAI_API_KEY`) or accept parameters directly
**Alternative (simpler)**: Use environment variables only:
@@ -249,7 +249,7 @@ Set the required environment variables:
```bash
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
export AZURE_OPENAI_MODEL="gpt-4o-mini"
# Optional: Set API key if not using DefaultAzureCredential
# export AZURE_OPENAI_API_KEY="your-api-key"
```
@@ -26,12 +26,12 @@ logger = logging.getLogger(__name__)
# Read required configuration
endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
deployment_name = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME")
model = os.environ.get("AZURE_OPENAI_MODEL")
if not endpoint:
raise ValueError("AZURE_OPENAI_ENDPOINT environment variable is required")
if not deployment_name:
raise ValueError("AZURE_OPENAI_DEPLOYMENT_NAME environment variable is required")
if not model:
raise ValueError("AZURE_OPENAI_MODEL environment variable is required")
# ============================================================================
@@ -121,7 +121,7 @@ agent = Agent(
instructions="You are a helpful assistant. Use get_weather for weather and get_time_zone for time zones.",
client=OpenAIChatCompletionClient(
azure_endpoint=endpoint,
model=deployment_name,
model=model,
),
tools=[get_time_zone], # ONLY server-side tools
)
+4 -1
View File
@@ -5,6 +5,9 @@ Integration with Anthropic's Claude API.
## Main Classes
- **`AnthropicClient`** - Chat client for Anthropic Claude models
- **`AnthropicFoundryClient`** - Anthropic chat client for Azure AI Foundry's Anthropic-compatible endpoint
- **`AnthropicBedrockClient`** - Anthropic chat client for Amazon Bedrock
- **`AnthropicVertexClient`** - Anthropic chat client for Google Vertex AI
- **`AnthropicChatOptions`** - Options TypedDict for Anthropic-specific parameters
## Usage
@@ -12,7 +15,7 @@ Integration with Anthropic's Claude API.
```python
from agent_framework.anthropic import AnthropicClient
client = AnthropicClient(model_id="claude-sonnet-4-20250514")
client = AnthropicClient(model="claude-sonnet-4-20250514")
response = await client.get_response("Hello")
```
+6
View File
@@ -10,6 +10,12 @@ pip install agent-framework-anthropic --pre
The Anthropic integration enables communication with the Anthropic API, allowing your Agent Framework applications to leverage Anthropic's capabilities.
The package also includes Anthropic-hosted transport wrappers for:
- Azure AI Foundry via `AnthropicFoundryClient`
- Amazon Bedrock via `AnthropicBedrockClient`
- Google Vertex AI via `AnthropicVertexClient`
### Basic Usage Example
See the [Anthropic agent examples](../../samples/02-agents/providers/anthropic/) which demonstrate:
@@ -2,7 +2,10 @@
import importlib.metadata
from ._bedrock_client import AnthropicBedrockClient, RawAnthropicBedrockClient
from ._chat_client import AnthropicChatOptions, AnthropicClient, RawAnthropicClient
from ._foundry_client import AnthropicFoundryClient, RawAnthropicFoundryClient
from ._vertex_client import AnthropicVertexClient, RawAnthropicVertexClient
try:
__version__ = importlib.metadata.version(__name__)
@@ -10,8 +13,14 @@ except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
__all__ = [
"AnthropicBedrockClient",
"AnthropicChatOptions",
"AnthropicClient",
"AnthropicFoundryClient",
"AnthropicVertexClient",
"RawAnthropicBedrockClient",
"RawAnthropicClient",
"RawAnthropicFoundryClient",
"RawAnthropicVertexClient",
"__version__",
]
@@ -0,0 +1,168 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, ClassVar, Generic, TypedDict
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
ChatAndFunctionMiddlewareTypes,
ChatMiddlewareLayer,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
)
from agent_framework._settings import SecretString, load_settings
from agent_framework.observability import ChatTelemetryLayer
from anthropic import AsyncAnthropicBedrock
from ._chat_client import AnthropicOptionsT, RawAnthropicClient
class AnthropicBedrockSettings(TypedDict, total=False):
"""Resolved settings for Anthropic Bedrock wrappers."""
aws_access_key_id: SecretString | None
aws_secret_access_key: SecretString | None
aws_region: str | None
aws_profile: str | None
aws_session_token: SecretString | None
anthropic_bedrock_base_url: str | None
anthropic_chat_model: str | None
class RawAnthropicBedrockClient(RawAnthropicClient[AnthropicOptionsT], Generic[AnthropicOptionsT]):
"""Raw Anthropic Bedrock chat client without middleware, telemetry, or function invocation support."""
OTEL_PROVIDER_NAME: ClassVar[str] = "aws.bedrock" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
*,
model: str | None = None,
aws_secret_key: str | None = None,
aws_access_key: str | None = None,
aws_region: str | None = None,
aws_profile: str | None = None,
aws_session_token: str | None = None,
base_url: str | None = None,
anthropic_client: AsyncAnthropicBedrock | None = None,
additional_beta_flags: list[str] | None = None,
additional_properties: dict[str, Any] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize a raw Anthropic Bedrock client.
Keyword Args:
model: The Anthropic model to use.
aws_secret_key: AWS secret access key.
aws_access_key: AWS access key ID.
aws_region: AWS region.
aws_profile: AWS profile name.
aws_session_token: AWS session token.
base_url: Optional custom Anthropic Bedrock base URL.
anthropic_client: Existing AsyncAnthropicBedrock client to reuse.
additional_beta_flags: Additional beta flags to enable on the client.
additional_properties: Additional properties stored on the client instance.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.
"""
settings = load_settings(
AnthropicBedrockSettings,
env_prefix="",
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
aws_region=aws_region,
aws_profile=aws_profile,
aws_session_token=aws_session_token,
anthropic_bedrock_base_url=base_url,
anthropic_chat_model=model,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
model_setting = settings.get("anthropic_chat_model")
access_key_secret = settings.get("aws_access_key_id")
secret_key_secret = settings.get("aws_secret_access_key")
session_token_secret = settings.get("aws_session_token")
if anthropic_client is None:
anthropic_client = AsyncAnthropicBedrock(
aws_secret_key=secret_key_secret.get_secret_value() if secret_key_secret is not None else None,
aws_access_key=access_key_secret.get_secret_value() if access_key_secret is not None else None,
aws_region=settings.get("aws_region"),
aws_profile=settings.get("aws_profile"),
aws_session_token=session_token_secret.get_secret_value() if session_token_secret is not None else None,
base_url=settings.get("anthropic_bedrock_base_url"),
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)
super().__init__(
model=model_setting,
anthropic_client=anthropic_client,
additional_beta_flags=additional_beta_flags,
additional_properties=additional_properties,
)
class AnthropicBedrockClient( # type: ignore[misc]
FunctionInvocationLayer[AnthropicOptionsT],
ChatMiddlewareLayer[AnthropicOptionsT],
ChatTelemetryLayer[AnthropicOptionsT],
RawAnthropicBedrockClient[AnthropicOptionsT],
Generic[AnthropicOptionsT],
):
"""Anthropic Bedrock chat client with middleware, telemetry, and function invocation support."""
def __init__(
self,
*,
model: str | None = None,
aws_secret_key: str | None = None,
aws_access_key: str | None = None,
aws_region: str | None = None,
aws_profile: str | None = None,
aws_session_token: str | None = None,
base_url: str | None = None,
anthropic_client: AsyncAnthropicBedrock | None = None,
additional_beta_flags: list[str] | 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 | None = None,
) -> None:
"""Initialize an Anthropic Bedrock client.
Keyword Args:
model: The Anthropic model to use.
aws_secret_key: AWS secret access key.
aws_access_key: AWS access key ID.
aws_region: AWS region.
aws_profile: AWS profile name.
aws_session_token: AWS session token.
base_url: Optional custom Anthropic Bedrock base URL.
anthropic_client: Existing AsyncAnthropicBedrock client to reuse.
additional_beta_flags: Additional beta flags to enable on the client.
additional_properties: Additional properties stored on the client instance.
middleware: Optional middleware to apply to the client.
function_invocation_configuration: Optional function invocation configuration override.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.
"""
super().__init__(
model=model,
aws_secret_key=aws_secret_key,
aws_access_key=aws_access_key,
aws_region=aws_region,
aws_profile=aws_profile,
aws_session_token=aws_session_token,
base_url=base_url,
anthropic_client=anthropic_client,
additional_beta_flags=additional_beta_flags,
additional_properties=additional_properties,
middleware=middleware,
function_invocation_configuration=function_invocation_configuration,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
@@ -31,7 +31,7 @@ from agent_framework._settings import SecretString, load_settings
from agent_framework._tools import SHELL_TOOL_KIND_VALUE
from agent_framework._types import _get_data_bytes_as_str # type: ignore
from agent_framework.observability import ChatTelemetryLayer
from anthropic import AsyncAnthropic
from anthropic import AsyncAnthropic, AsyncAnthropicBedrock, AsyncAnthropicFoundry, AsyncAnthropicVertex
from anthropic.types.beta import (
BetaContentBlock,
BetaMessage,
@@ -79,6 +79,7 @@ BETA_FLAGS: Final[list[str]] = ["mcp-client-2025-04-04", "code-execution-2025-08
STRUCTURED_OUTPUTS_BETA_FLAG: Final[str] = "structured-outputs-2025-11-13"
ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None)
AnthropicAsyncClient = AsyncAnthropic | AsyncAnthropicBedrock | AsyncAnthropicFoundry | AsyncAnthropicVertex
# region Anthropic Chat Options TypedDict
@@ -113,8 +114,6 @@ class AnthropicChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT],
a default of 1024 will be used.
Keys:
model_id: The model to use for the request,
translates to ``model`` in Anthropic API.
temperature: Sampling temperature between 0 and 1.
top_p: Nucleus sampling parameter.
max_tokens: Maximum number of tokens to generate (REQUIRED).
@@ -169,12 +168,24 @@ AnthropicOptionsT = TypeVar(
# Translation between framework options keys and Anthropic Messages API
OPTION_TRANSLATIONS: dict[str, str] = {
"model_id": "model",
"stop": "stop_sequences",
"instructions": "system",
}
def _apply_option_translations(options: dict[str, Any]) -> None:
"""Translate framework option keys to Anthropic request keys in-place.
When both the old and new key are present, the new key wins and the old key
is discarded to preserve explicit overrides.
"""
for old_key, new_key in OPTION_TRANSLATIONS.items():
if old_key not in options or old_key == new_key:
continue
old_value = options.pop(old_key)
options.setdefault(new_key, old_value)
# region Role and Finish Reason Maps
@@ -204,11 +215,11 @@ class AnthropicSettings(TypedDict, total=False):
Keys:
api_key: The Anthropic API key.
chat_model_id: The Anthropic chat model ID.
chat_model: The Anthropic chat model.
"""
api_key: SecretString | None
chat_model_id: str | None
chat_model: str | None
class RawAnthropicClient(
@@ -236,8 +247,8 @@ class RawAnthropicClient(
self,
*,
api_key: str | None = None,
model_id: str | None = None,
anthropic_client: AsyncAnthropic | None = None,
model: str | None = None,
anthropic_client: AnthropicAsyncClient | None = None,
additional_beta_flags: list[str] | None = None,
additional_properties: dict[str, Any] | None = None,
env_file_path: str | None = None,
@@ -247,7 +258,7 @@ class RawAnthropicClient(
Keyword Args:
api_key: The Anthropic API key to use for authentication.
model_id: The ID of the model to use.
model: The model to use.
anthropic_client: An existing Anthropic client to use. If not provided, one will be created.
This can be used to further configure the client before passing it in.
For instance if you need to set a different base_url for testing or private deployments.
@@ -265,11 +276,11 @@ class RawAnthropicClient(
# Using environment variables
# Set ANTHROPIC_API_KEY=your_anthropic_api_key
# ANTHROPIC_CHAT_MODEL_ID=claude-sonnet-4-5-20250929
# ANTHROPIC_CHAT_MODEL=claude-sonnet-4-5-20250929
# Or passing parameters directly
client = RawAnthropicClient(
model_id="claude-sonnet-4-5-20250929",
model="claude-sonnet-4-5-20250929",
api_key="your_anthropic_api_key",
)
@@ -283,7 +294,7 @@ class RawAnthropicClient(
api_key="your_anthropic_api_key", base_url="https://custom-anthropic-endpoint.com"
)
client = RawAnthropicClient(
model_id="claude-sonnet-4-5-20250929",
model="claude-sonnet-4-5-20250929",
anthropic_client=anthropic_client,
)
@@ -296,7 +307,7 @@ class RawAnthropicClient(
my_custom_option: str
client: RawAnthropicClient[MyOptions] = RawAnthropicClient(model_id="claude-sonnet-4-5-20250929")
client: RawAnthropicClient[MyOptions] = RawAnthropicClient(model="claude-sonnet-4-5-20250929")
response = await client.get_response("Hello", options={"my_custom_option": "value"})
"""
@@ -304,13 +315,13 @@ class RawAnthropicClient(
AnthropicSettings,
env_prefix="ANTHROPIC_",
api_key=api_key,
chat_model_id=model_id,
chat_model=model,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
api_key_secret = anthropic_settings.get("api_key")
model_id_setting = anthropic_settings.get("chat_model_id")
model_setting = anthropic_settings.get("chat_model")
if anthropic_client is None:
if api_key_secret is None:
@@ -332,7 +343,7 @@ class RawAnthropicClient(
# Initialize instance variables
self.anthropic_client = anthropic_client
self.additional_beta_flags = additional_beta_flags or []
self.model_id = model_id_setting
self.model = model_setting
# streaming requires tracking the last function call ID, name, and content type
self._last_call_id_name: tuple[str, str] | None = None
self._last_call_content_type: str | None = None
@@ -513,7 +524,7 @@ class RawAnthropicClient(
if stream:
# Streaming mode
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
async for chunk in await self.anthropic_client.beta.messages.create(**run_options, stream=True):
async for chunk in await self.anthropic_client.beta.messages.create(**run_options, stream=True): # type: ignore[misc]
parsed_chunk = self._process_stream_event(chunk)
if parsed_chunk:
yield parsed_chunk
@@ -522,7 +533,7 @@ class RawAnthropicClient(
# Non-streaming mode
async def _get_response() -> ChatResponse:
message = await self.anthropic_client.beta.messages.create(**run_options, stream=False)
message = await self.anthropic_client.beta.messages.create(**run_options, stream=False) # type: ignore[misc]
return self._process_message(message, options)
return _get_response()
@@ -561,16 +572,22 @@ class RawAnthropicClient(
# Stream mode is controlled explicitly at call sites.
run_options.pop("stream", None)
# Translation between options keys and Anthropic Messages API
for old_key, new_key in OPTION_TRANSLATIONS.items():
if old_key in run_options and old_key != new_key:
run_options[new_key] = run_options.pop(old_key)
_apply_option_translations(run_options)
# model id
# Filter out framework kwargs that should not be passed to the Anthropic API.
# This includes underscore-prefixed internal objects (like _function_middleware_pipeline)
# and framework kwargs like 'thread' and 'middleware'.
filtered_kwargs = {
k: v for k, v in kwargs.items() if not k.startswith("_") and k not in {"thread", "middleware"}
}
_apply_option_translations(filtered_kwargs)
run_options.update(filtered_kwargs)
# model
if not run_options.get("model"):
if not self.model_id:
raise ValueError("model_id must be a non-empty string")
run_options["model"] = self.model_id
if not self.model:
raise ValueError("model must be a non-empty string")
run_options["model"] = self.model
# max_tokens - Anthropic requires this, default if not provided
if not run_options.get("max_tokens"):
@@ -607,13 +624,6 @@ class RawAnthropicClient(
# Add the structured outputs beta flag
run_options["betas"].add(STRUCTURED_OUTPUTS_BETA_FLAG)
# Filter out framework kwargs that should not be passed to the Anthropic API.
# This includes underscore-prefixed internal objects (like _function_middleware_pipeline)
# and framework kwargs like 'thread' and 'middleware'.
filtered_kwargs = {
k: v for k, v in kwargs.items() if not k.startswith("_") and k not in {"thread", "middleware"}
}
run_options.update(filtered_kwargs)
return run_options
def _prepare_betas(self, options: Mapping[str, Any]) -> set[str]:
@@ -918,7 +928,7 @@ class RawAnthropicClient(
)
],
usage_details=self._parse_usage_from_anthropic(message.usage),
model_id=message.model,
model=message.model,
finish_reason=FINISH_REASON_MAP.get(message.stop_reason) if message.stop_reason else None,
response_format=options.get("response_format"),
raw_representation=message,
@@ -946,7 +956,7 @@ class RawAnthropicClient(
*self._parse_contents_from_anthropic(event.message.content),
*usage_details,
],
model_id=event.message.model,
model=event.message.model,
finish_reason=FINISH_REASON_MAP.get(event.message.stop_reason)
if event.message.stop_reason
else None,
@@ -1396,8 +1406,8 @@ class AnthropicClient(
self,
*,
api_key: str | None = None,
model_id: str | None = None,
anthropic_client: AsyncAnthropic | None = None,
model: str | None = None,
anthropic_client: AnthropicAsyncClient | None = None,
additional_beta_flags: list[str] | None = None,
additional_properties: dict[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
@@ -1409,7 +1419,7 @@ class AnthropicClient(
Keyword Args:
api_key: The Anthropic API key to use for authentication.
model_id: The ID of the model to use.
model: The model to use.
anthropic_client: An existing Anthropic client to use. If not provided, one will be created.
This can be used to further configure the client before passing it in.
For instance if you need to set a different base_url for testing or private deployments.
@@ -1428,11 +1438,11 @@ class AnthropicClient(
# Using environment variables
# Set ANTHROPIC_API_KEY=your_anthropic_api_key
# ANTHROPIC_CHAT_MODEL_ID=claude-sonnet-4-5-20250929
# ANTHROPIC_CHAT_MODEL=claude-sonnet-4-5-20250929
# Or passing parameters directly
client = AnthropicClient(
model_id="claude-sonnet-4-5-20250929",
model="claude-sonnet-4-5-20250929",
api_key="your_anthropic_api_key",
)
@@ -1446,7 +1456,7 @@ class AnthropicClient(
api_key="your_anthropic_api_key", base_url="https://custom-anthropic-endpoint.com"
)
client = AnthropicClient(
model_id="claude-sonnet-4-5-20250929",
model="claude-sonnet-4-5-20250929",
anthropic_client=anthropic_client,
)
@@ -1459,12 +1469,12 @@ class AnthropicClient(
my_custom_option: str
client: AnthropicClient[MyOptions] = AnthropicClient(model_id="claude-sonnet-4-5-20250929")
client: AnthropicClient[MyOptions] = AnthropicClient(model="claude-sonnet-4-5-20250929")
response = await client.get_response("Hello", options={"my_custom_option": "value"})
"""
super().__init__(
api_key=api_key,
model_id=model_id,
model=model,
anthropic_client=anthropic_client,
additional_beta_flags=additional_beta_flags,
additional_properties=additional_properties,
@@ -0,0 +1,166 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from collections.abc import Awaitable, Callable, Sequence
from typing import Any, ClassVar, Generic, TypedDict
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
ChatAndFunctionMiddlewareTypes,
ChatMiddlewareLayer,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
)
from agent_framework._settings import SecretString, load_settings
from agent_framework.observability import ChatTelemetryLayer
from anthropic import AsyncAnthropicFoundry
from ._chat_client import AnthropicOptionsT, RawAnthropicClient
AnthropicFoundryAzureADTokenProvider = Callable[[], str | Awaitable[str]]
class AnthropicFoundrySettings(TypedDict, total=False):
"""Resolved settings for Anthropic Foundry wrappers."""
anthropic_foundry_api_key: SecretString | None
anthropic_foundry_resource: str | None
anthropic_foundry_base_url: str | None
anthropic_chat_model: str | None
class RawAnthropicFoundryClient(RawAnthropicClient[AnthropicOptionsT], Generic[AnthropicOptionsT]):
"""Raw Anthropic Foundry chat client without middleware, telemetry, or function invocation support."""
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.foundry" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
*,
model: str | None = None,
resource: str | None = None,
api_key: str | None = None,
azure_ad_token_provider: AnthropicFoundryAzureADTokenProvider | None = None,
base_url: str | None = None,
anthropic_client: AsyncAnthropicFoundry | None = None,
additional_beta_flags: list[str] | None = None,
additional_properties: dict[str, Any] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize a raw Anthropic Foundry client.
Keyword Args:
model: The Anthropic model to use.
resource: The Foundry resource name.
api_key: The Foundry Anthropic API key.
azure_ad_token_provider: Azure AD token provider used by the Anthropic SDK.
base_url: Full Anthropic-compatible Foundry base URL.
anthropic_client: Existing AsyncAnthropicFoundry client to reuse.
additional_beta_flags: Additional beta flags to enable on the client.
additional_properties: Additional properties stored on the client instance.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.
"""
settings = load_settings(
AnthropicFoundrySettings,
env_prefix="",
anthropic_foundry_api_key=api_key,
anthropic_foundry_resource=resource,
anthropic_foundry_base_url=base_url,
anthropic_chat_model=model,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
api_key_secret = settings.get("anthropic_foundry_api_key")
model_setting = settings.get("anthropic_chat_model")
resource_setting = settings.get("anthropic_foundry_resource")
base_url_setting = settings.get("anthropic_foundry_base_url")
api_key_value = api_key_secret.get_secret_value() if api_key_secret is not None else None
if anthropic_client is None:
if base_url_setting is None and resource_setting is None:
message = (
"Anthropic Foundry requires either `resource`/`ANTHROPIC_FOUNDRY_RESOURCE` "
"or `base_url`/`ANTHROPIC_FOUNDRY_BASE_URL`."
)
raise ValueError(message)
if base_url_setting is not None:
anthropic_client = AsyncAnthropicFoundry(
base_url=base_url_setting,
api_key=api_key_value,
azure_ad_token_provider=azure_ad_token_provider,
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)
else:
anthropic_client = AsyncAnthropicFoundry(
resource=resource_setting,
api_key=api_key_value,
azure_ad_token_provider=azure_ad_token_provider,
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)
super().__init__(
model=model_setting,
anthropic_client=anthropic_client,
additional_beta_flags=additional_beta_flags,
additional_properties=additional_properties,
)
class AnthropicFoundryClient( # type: ignore[misc]
FunctionInvocationLayer[AnthropicOptionsT],
ChatMiddlewareLayer[AnthropicOptionsT],
ChatTelemetryLayer[AnthropicOptionsT],
RawAnthropicFoundryClient[AnthropicOptionsT],
Generic[AnthropicOptionsT],
):
"""Anthropic Foundry chat client with middleware, telemetry, and function invocation support."""
def __init__(
self,
*,
model: str | None = None,
resource: str | None = None,
api_key: str | None = None,
azure_ad_token_provider: AnthropicFoundryAzureADTokenProvider | None = None,
base_url: str | None = None,
anthropic_client: AsyncAnthropicFoundry | None = None,
additional_beta_flags: list[str] | 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 | None = None,
) -> None:
"""Initialize an Anthropic Foundry client.
Keyword Args:
model: The Anthropic model to use.
resource: The Foundry resource name.
api_key: The Foundry Anthropic API key.
azure_ad_token_provider: Azure AD token provider used by the Anthropic SDK.
base_url: Full Anthropic-compatible Foundry base URL.
anthropic_client: Existing AsyncAnthropicFoundry client to reuse.
additional_beta_flags: Additional beta flags to enable on the client.
additional_properties: Additional properties stored on the client instance.
middleware: Optional middleware to apply to the client.
function_invocation_configuration: Optional function invocation configuration override.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.
"""
super().__init__(
model=model,
resource=resource,
api_key=api_key,
azure_ad_token_provider=azure_ad_token_provider,
base_url=base_url,
anthropic_client=anthropic_client,
additional_beta_flags=additional_beta_flags,
additional_properties=additional_properties,
middleware=middleware,
function_invocation_configuration=function_invocation_configuration,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
@@ -0,0 +1,160 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
ChatAndFunctionMiddlewareTypes,
ChatMiddlewareLayer,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
)
from agent_framework._settings import load_settings
from agent_framework.observability import ChatTelemetryLayer
from anthropic import NOT_GIVEN, AsyncAnthropicVertex
from ._chat_client import AnthropicOptionsT, RawAnthropicClient
if TYPE_CHECKING:
from google.auth.credentials import Credentials as GoogleCredentials
class AnthropicVertexSettings(TypedDict, total=False):
"""Resolved settings for Anthropic Vertex wrappers."""
cloud_ml_region: str | None
anthropic_vertex_project_id: str | None
anthropic_vertex_base_url: str | None
anthropic_chat_model: str | None
class RawAnthropicVertexClient(RawAnthropicClient[AnthropicOptionsT], Generic[AnthropicOptionsT]):
"""Raw Anthropic Vertex chat client without middleware, telemetry, or function invocation support."""
OTEL_PROVIDER_NAME: ClassVar[str] = "google.vertex.ai" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
*,
model: str | None = None,
region: str | None = None,
project_id: str | None = None,
access_token: str | None = None,
credentials: GoogleCredentials | None = None,
base_url: str | None = None,
anthropic_client: AsyncAnthropicVertex | None = None,
additional_beta_flags: list[str] | None = None,
additional_properties: dict[str, Any] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize a raw Anthropic Vertex client.
Keyword Args:
model: The Anthropic model to use.
region: Vertex region. Falls back to `CLOUD_ML_REGION`.
project_id: Vertex project ID. Falls back to `ANTHROPIC_VERTEX_PROJECT_ID`.
access_token: Explicit OAuth access token.
credentials: Google credentials object.
base_url: Optional custom Anthropic Vertex base URL.
anthropic_client: Existing AsyncAnthropicVertex client to reuse.
additional_beta_flags: Additional beta flags to enable on the client.
additional_properties: Additional properties stored on the client instance.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.
"""
settings = load_settings(
AnthropicVertexSettings,
env_prefix="",
cloud_ml_region=region,
anthropic_vertex_project_id=project_id,
anthropic_vertex_base_url=base_url,
anthropic_chat_model=model,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
model_setting = settings.get("anthropic_chat_model")
region_setting = settings.get("cloud_ml_region")
project_id_setting = settings.get("anthropic_vertex_project_id")
if anthropic_client is None:
resolved_region = region_setting if region_setting is not None else NOT_GIVEN
resolved_project_id = project_id_setting if project_id_setting is not None else NOT_GIVEN
anthropic_client = AsyncAnthropicVertex(
region=resolved_region,
project_id=resolved_project_id,
access_token=access_token,
credentials=credentials,
base_url=settings.get("anthropic_vertex_base_url"),
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)
super().__init__(
model=model_setting,
anthropic_client=anthropic_client,
additional_beta_flags=additional_beta_flags,
additional_properties=additional_properties,
)
class AnthropicVertexClient( # type: ignore[misc]
FunctionInvocationLayer[AnthropicOptionsT],
ChatMiddlewareLayer[AnthropicOptionsT],
ChatTelemetryLayer[AnthropicOptionsT],
RawAnthropicVertexClient[AnthropicOptionsT],
Generic[AnthropicOptionsT],
):
"""Anthropic Vertex chat client with middleware, telemetry, and function invocation support."""
def __init__(
self,
*,
model: str | None = None,
region: str | None = None,
project_id: str | None = None,
access_token: str | None = None,
credentials: GoogleCredentials | None = None,
base_url: str | None = None,
anthropic_client: AsyncAnthropicVertex | None = None,
additional_beta_flags: list[str] | 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 | None = None,
) -> None:
"""Initialize an Anthropic Vertex client.
Keyword Args:
model: The Anthropic model to use.
region: Vertex region. Falls back to `CLOUD_ML_REGION`.
project_id: Vertex project ID. Falls back to `ANTHROPIC_VERTEX_PROJECT_ID`.
access_token: Explicit OAuth access token.
credentials: Google credentials object.
base_url: Optional custom Anthropic Vertex base URL.
anthropic_client: Existing AsyncAnthropicVertex client to reuse.
additional_beta_flags: Additional beta flags to enable on the client.
additional_properties: Additional properties stored on the client instance.
middleware: Optional middleware to apply to the client.
function_invocation_configuration: Optional function invocation configuration override.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.
"""
super().__init__(
model=model,
region=region,
project_id=project_id,
access_token=access_token,
credentials=credentials,
base_url=base_url,
anthropic_client=anthropic_client,
additional_beta_flags=additional_beta_flags,
additional_properties=additional_properties,
middleware=middleware,
function_invocation_configuration=function_invocation_configuration,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
+1 -1
View File
@@ -28,7 +28,7 @@ def anthropic_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
env_vars = {
"ANTHROPIC_API_KEY": "test-api-key-12345",
"ANTHROPIC_CHAT_MODEL_ID": "claude-3-5-sonnet-20241022",
"ANTHROPIC_CHAT_MODEL": "claude-3-5-sonnet-20241022",
}
env_vars.update(override_env_param_dict) # type: ignore
@@ -40,7 +40,7 @@ skip_if_anthropic_integration_tests_disabled = pytest.mark.skipif(
def create_test_anthropic_client(
mock_anthropic_client: MagicMock,
model_id: str | None = None,
model: str | None = None,
anthropic_settings: AnthropicSettings | None = None,
) -> AnthropicClient:
"""Helper function to create AnthropicClient instances for testing, bypassing normal validation."""
@@ -51,7 +51,7 @@ def create_test_anthropic_client(
AnthropicSettings,
env_prefix="ANTHROPIC_",
api_key="test-api-key-12345",
chat_model_id="claude-3-5-sonnet-20241022",
chat_model="claude-3-5-sonnet-20241022",
)
# Create client instance directly
@@ -59,7 +59,7 @@ def create_test_anthropic_client(
# Set attributes directly
client.anthropic_client = mock_anthropic_client
client.model_id = model_id or anthropic_settings["chat_model_id"]
client.model = model or anthropic_settings["chat_model"]
client._last_call_id_name = None
client._tool_name_aliases = {}
client.additional_properties = {}
@@ -83,7 +83,7 @@ def test_anthropic_settings_init(anthropic_unit_test_env: dict[str, str]) -> Non
assert settings["api_key"] is not None
assert settings["api_key"].get_secret_value() == anthropic_unit_test_env["ANTHROPIC_API_KEY"]
assert settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]
assert settings["chat_model"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"]
def test_anthropic_settings_init_with_explicit_values() -> None:
@@ -92,12 +92,12 @@ def test_anthropic_settings_init_with_explicit_values() -> None:
AnthropicSettings,
env_prefix="ANTHROPIC_",
api_key="custom-api-key",
chat_model_id="claude-3-opus-20240229",
chat_model="claude-3-opus-20240229",
)
assert settings["api_key"] is not None
assert settings["api_key"].get_secret_value() == "custom-api-key"
assert settings["chat_model_id"] == "claude-3-opus-20240229"
assert settings["chat_model"] == "claude-3-opus-20240229"
@pytest.mark.parametrize("exclude_list", [["ANTHROPIC_API_KEY"]], indirect=True)
@@ -107,7 +107,7 @@ def test_anthropic_settings_missing_api_key(
"""Test AnthropicSettings when API key is missing."""
settings = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_")
assert settings["api_key"] is None
assert settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]
assert settings["chat_model"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"]
# Client Initialization Tests
@@ -115,10 +115,10 @@ def test_anthropic_settings_missing_api_key(
def test_anthropic_client_init_with_client(mock_anthropic_client: MagicMock) -> None:
"""Test AnthropicClient initialization with existing anthropic_client."""
client = create_test_anthropic_client(mock_anthropic_client, model_id="claude-3-5-sonnet-20241022")
client = create_test_anthropic_client(mock_anthropic_client, model="claude-3-5-sonnet-20241022")
assert client.anthropic_client is mock_anthropic_client
assert client.model_id == "claude-3-5-sonnet-20241022"
assert client.model == "claude-3-5-sonnet-20241022"
assert isinstance(client, SupportsChatGetResponse)
@@ -141,11 +141,11 @@ def test_anthropic_client_init_auto_create_client(
"""Test AnthropicClient initialization with auto-created anthropic_client."""
client = AnthropicClient(
api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"],
model_id=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"],
model=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"],
)
assert client.anthropic_client is not None
assert client.model_id == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]
assert client.model == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"]
def test_anthropic_client_init_missing_api_key() -> None:
@@ -153,7 +153,7 @@ def test_anthropic_client_init_missing_api_key() -> None:
with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load:
mock_load.return_value = {
"api_key": None,
"chat_model_id": "claude-3-5-sonnet-20241022",
"chat_model": "claude-3-5-sonnet-20241022",
}
with pytest.raises(ValueError, match="Anthropic API key is required"):
@@ -740,7 +740,7 @@ async def test_prepare_options_basic(mock_anthropic_client: MagicMock) -> None:
run_options = client._prepare_options(messages, chat_options)
assert run_options["model"] == client.model_id
assert run_options["model"] == client.model
assert run_options["max_tokens"] == 100
assert run_options["temperature"] == 0.7
assert "messages" in run_options
@@ -980,7 +980,7 @@ def test_process_message_basic(mock_anthropic_client: MagicMock) -> None:
response = client._process_message(mock_message, {})
assert response.response_id == "msg_123"
assert response.model_id == "claude-3-5-sonnet-20241022"
assert response.model == "claude-3-5-sonnet-20241022"
assert len(response.messages) == 1
assert response.messages[0].role == "assistant"
assert len(response.messages[0].contents) == 1
@@ -2036,10 +2036,10 @@ def test_prepare_options_with_instructions(mock_anthropic_client: MagicMock) ->
assert result["max_tokens"] == 1024
def test_prepare_options_missing_model_id(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options raises error when model_id is missing."""
def test_prepare_options_missing_model(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options raises error when model is missing."""
client = create_test_anthropic_client(mock_anthropic_client)
client.model_id = "" # Set empty model_id
client.model = "" # Set empty model
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
options = {}
@@ -2048,7 +2048,31 @@ def test_prepare_options_missing_model_id(mock_anthropic_client: MagicMock) -> N
client._prepare_options(messages, options)
raise AssertionError("Expected ValueError")
except ValueError as e:
assert "model_id must be a non-empty string" in str(e)
assert "model must be a non-empty string" in str(e)
def test_prepare_options_translates_model_option(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options translates model to model for runtime option compatibility."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
result = client._prepare_options(messages, {"model": "claude-3-5-sonnet-20241022"})
assert result["model"] == "claude-3-5-sonnet-20241022"
assert "model_id" not in result
def test_prepare_options_translates_model_kwarg(mock_anthropic_client: MagicMock) -> None:
"""Test prepare_options translates model passed as a direct keyword argument."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [Message(role="user", contents=[Content.from_text("Hello")])]
result = client._prepare_options(messages, {}, model="claude-3-5-sonnet-20241022")
assert result["model"] == "claude-3-5-sonnet-20241022"
assert "model_id" not in result
def test_prepare_options_with_user_metadata(mock_anthropic_client: MagicMock) -> None:
@@ -0,0 +1,156 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import AGENT_FRAMEWORK_USER_AGENT, ChatMiddlewareLayer, FunctionInvocationLayer
from agent_framework.observability import ChatTelemetryLayer
from agent_framework_anthropic import (
AnthropicBedrockClient,
AnthropicFoundryClient,
AnthropicVertexClient,
RawAnthropicBedrockClient,
RawAnthropicFoundryClient,
RawAnthropicVertexClient,
)
def _create_mock_transport(base_url: str) -> MagicMock:
transport = MagicMock()
transport.base_url = base_url
transport.beta = MagicMock()
transport.beta.messages = MagicMock()
transport.beta.messages.create = AsyncMock()
return transport
@pytest.mark.parametrize(
("public_client", "raw_client"),
[
(AnthropicFoundryClient, RawAnthropicFoundryClient),
(AnthropicBedrockClient, RawAnthropicBedrockClient),
(AnthropicVertexClient, RawAnthropicVertexClient),
],
)
def test_provider_client_wraps_raw_client_with_standard_layer_order(public_client, raw_client) -> None:
assert issubclass(public_client, raw_client)
mro = public_client.__mro__
assert mro.index(FunctionInvocationLayer) < mro.index(ChatMiddlewareLayer)
assert mro.index(ChatMiddlewareLayer) < mro.index(ChatTelemetryLayer)
assert mro.index(ChatTelemetryLayer) < mro.index(raw_client)
def test_raw_anthropic_foundry_client_creates_sdk_client_from_settings(tmp_path) -> None:
env_file = tmp_path / ".env"
env_file.write_text(
"ANTHROPIC_CHAT_MODEL=claude-foundry-test\n"
"ANTHROPIC_FOUNDRY_API_KEY=test-key\n"
"ANTHROPIC_FOUNDRY_RESOURCE=test-resource\n"
)
mock_transport = _create_mock_transport("https://test-resource.services.ai.azure.com/anthropic/")
with patch(
"agent_framework_anthropic._foundry_client.AsyncAnthropicFoundry", return_value=mock_transport
) as factory:
client = RawAnthropicFoundryClient(env_file_path=str(env_file))
assert client.model == "claude-foundry-test"
assert client.anthropic_client is mock_transport
factory.assert_called_once_with(
resource="test-resource",
api_key="test-key",
azure_ad_token_provider=None,
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)
def test_raw_anthropic_foundry_client_creates_sdk_client_from_base_url_settings(tmp_path) -> None:
env_file = tmp_path / ".env"
env_file.write_text(
"ANTHROPIC_CHAT_MODEL=claude-foundry-test\n"
"ANTHROPIC_FOUNDRY_API_KEY=test-key\n"
"ANTHROPIC_FOUNDRY_BASE_URL=https://test-resource.services.ai.azure.com/anthropic/\n"
)
mock_transport = _create_mock_transport("https://test-resource.services.ai.azure.com/anthropic/")
with patch(
"agent_framework_anthropic._foundry_client.AsyncAnthropicFoundry", return_value=mock_transport
) as factory:
client = RawAnthropicFoundryClient(env_file_path=str(env_file))
assert client.model == "claude-foundry-test"
assert client.anthropic_client is mock_transport
factory.assert_called_once_with(
base_url="https://test-resource.services.ai.azure.com/anthropic/",
api_key="test-key",
azure_ad_token_provider=None,
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)
def test_raw_anthropic_foundry_client_requires_resource_or_base_url() -> None:
with patch("agent_framework_anthropic._foundry_client.load_settings") as mock_load:
mock_load.return_value = {
"anthropic_foundry_api_key": None,
"anthropic_foundry_resource": None,
"anthropic_foundry_base_url": None,
"anthropic_chat_model": None,
}
with pytest.raises(
ValueError,
match=(
"Anthropic Foundry requires either `resource`/`ANTHROPIC_FOUNDRY_RESOURCE` "
"or `base_url`/`ANTHROPIC_FOUNDRY_BASE_URL`\\."
),
):
RawAnthropicFoundryClient()
def test_raw_anthropic_bedrock_client_creates_sdk_client_from_arguments() -> None:
mock_transport = _create_mock_transport("https://bedrock-runtime.us-east-1.amazonaws.com")
with patch(
"agent_framework_anthropic._bedrock_client.AsyncAnthropicBedrock", return_value=mock_transport
) as factory:
client = RawAnthropicBedrockClient(
model="claude-bedrock-test",
aws_access_key="access-key",
aws_secret_key="secret-key",
aws_region="us-east-1",
)
assert client.model == "claude-bedrock-test"
assert client.anthropic_client is mock_transport
factory.assert_called_once_with(
aws_secret_key="secret-key",
aws_access_key="access-key",
aws_region="us-east-1",
aws_profile=None,
aws_session_token=None,
base_url=None,
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)
def test_raw_anthropic_vertex_client_creates_sdk_client_from_arguments() -> None:
mock_transport = _create_mock_transport("https://us-central1-aiplatform.googleapis.com/v1")
with patch("agent_framework_anthropic._vertex_client.AsyncAnthropicVertex", return_value=mock_transport) as factory:
client = RawAnthropicVertexClient(
model="claude-vertex-test",
region="us-central1",
project_id="test-project",
)
assert client.model == "claude-vertex-test"
assert client.anthropic_client is mock_transport
factory.assert_called_once_with(
region="us-central1",
project_id="test-project",
access_token=None,
credentials=None,
base_url=None,
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)
@@ -180,8 +180,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: EmbeddingFunction | None = None,
context_prompt: str | None = None,
azure_openai_resource_url: str | None = None,
model_deployment_name: str | None = None,
model_name: str | None = None,
model: str | None = None,
knowledge_base_name: None = None,
retrieval_instructions: str | None = None,
azure_openai_api_key: str | None = None,
@@ -206,8 +205,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: Embedding provider used for vector search.
context_prompt: Custom prompt to prepend to retrieved context.
azure_openai_resource_url: Unused in semantic mode.
model_deployment_name: Unused in semantic mode.
model_name: Unused in semantic mode.
model: Unused in semantic mode.
knowledge_base_name: Must be ``None`` for this overload.
retrieval_instructions: Unused in semantic mode.
azure_openai_api_key: Unused in semantic mode.
@@ -235,8 +233,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: EmbeddingFunction | None = None,
context_prompt: str | None = None,
azure_openai_resource_url: str,
model_deployment_name: str,
model_name: str | None = None,
model: str,
knowledge_base_name: None = None,
retrieval_instructions: str | None = None,
azure_openai_api_key: str | None = None,
@@ -261,8 +258,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: Embedding provider used for vector search.
context_prompt: Custom prompt to prepend to retrieved context.
azure_openai_resource_url: Azure OpenAI resource URL for Knowledge Base creation.
model_deployment_name: Azure OpenAI deployment used by the generated Knowledge Base.
model_name: Underlying model name for the Knowledge Base model configuration.
model: Model used by the generated Knowledge Base.
knowledge_base_name: Must be ``None`` for this overload.
retrieval_instructions: Custom instructions for Knowledge Base retrieval.
azure_openai_api_key: Optional Azure OpenAI API key for Knowledge Base creation.
@@ -290,8 +286,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: EmbeddingFunction | None = None,
context_prompt: str | None = None,
azure_openai_resource_url: str | None = None,
model_deployment_name: str | None = None,
model_name: str | None = None,
model: str | None = None,
knowledge_base_name: str,
retrieval_instructions: str | None = None,
azure_openai_api_key: str | None = None,
@@ -317,8 +312,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: Embedding provider used for vector search.
context_prompt: Custom prompt to prepend to retrieved context.
azure_openai_resource_url: Unused when connecting to an existing Knowledge Base.
model_deployment_name: Unused when connecting to an existing Knowledge Base.
model_name: Unused when connecting to an existing Knowledge Base.
model: Unused when connecting to an existing Knowledge Base.
retrieval_instructions: Custom instructions for Knowledge Base retrieval.
azure_openai_api_key: Unused when connecting to an existing Knowledge Base.
knowledge_base_output_mode: Output mode for Knowledge Base retrieval.
@@ -345,8 +339,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: EmbeddingFunction | None = None,
context_prompt: str | None = None,
azure_openai_resource_url: str | None = None,
model_deployment_name: str | None = None,
model_name: str | None = None,
model: str | None = None,
knowledge_base_name: None = None,
retrieval_instructions: str | None = None,
azure_openai_api_key: str | None = None,
@@ -375,8 +368,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: Embedding provider used for vector search.
context_prompt: Custom prompt to prepend to retrieved context.
azure_openai_resource_url: Azure OpenAI resource URL when creating a Knowledge Base from an index.
model_deployment_name: Azure OpenAI deployment when creating a Knowledge Base from an index.
model_name: Underlying model name for Knowledge Base model configuration.
model: Model used when creating a Knowledge Base from an index.
knowledge_base_name: Resolved from ``env_file_path`` or ``AZURE_SEARCH_KNOWLEDGE_BASE_NAME``.
retrieval_instructions: Custom instructions for Knowledge Base retrieval.
azure_openai_api_key: Optional Azure OpenAI API key for Knowledge Base creation.
@@ -403,8 +395,7 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: EmbeddingFunction | None = None,
context_prompt: str | None = None,
azure_openai_resource_url: str | None = None,
model_deployment_name: str | None = None,
model_name: str | None = None,
model: str | None = None,
knowledge_base_name: str | None = None,
retrieval_instructions: str | None = None,
azure_openai_api_key: str | None = None,
@@ -432,11 +423,8 @@ class AzureAISearchContextProvider(ContextProvider):
embedding_function: Async function to generate embeddings or a SupportsGetEmbeddings instance.
context_prompt: Custom prompt to prepend to retrieved context.
azure_openai_resource_url: Azure OpenAI resource URL for Knowledge Base.
model_deployment_name: Model deployment name in Azure OpenAI.
model_name: The underlying model name.
knowledge_base_name: Name of an existing Knowledge Base to use. In agentic mode,
providing this explicitly selects the Knowledge Base-backed setup and ignores any
environment-provided index name.
model: Model name to use for Azure OpenAI vectorization.
knowledge_base_name: Name of an existing Knowledge Base to use.
retrieval_instructions: Custom instructions for Knowledge Base retrieval.
azure_openai_api_key: Azure OpenAI API key.
knowledge_base_output_mode: Output mode for Knowledge Base retrieval.
@@ -483,10 +471,8 @@ class AzureAISearchContextProvider(ContextProvider):
if ignored_agentic_field is not None:
settings[ignored_agentic_field] = None
if mode == "agentic" and settings.get("index_name") and not model_deployment_name:
raise ValueError(
"model_deployment_name is required for agentic mode when creating Knowledge Base from index."
)
if mode == "agentic" and settings.get("index_name") and not model:
raise ValueError("model is required for agentic mode when creating Knowledge Base from index.")
resolved_credential: AzureKeyCredential | AsyncTokenCredential
if credential:
@@ -512,8 +498,7 @@ class AzureAISearchContextProvider(ContextProvider):
self.context_prompt = context_prompt or self._DEFAULT_SEARCH_CONTEXT_PROMPT
self.azure_openai_resource_url = azure_openai_resource_url
self.azure_openai_deployment_name = model_deployment_name
self.model_name = model_name or model_deployment_name
self.azure_openai_model = model
self.knowledge_base_name = settings.get("knowledge_base_name")
self.retrieval_instructions = retrieval_instructions
self.azure_openai_api_key = azure_openai_api_key
@@ -762,8 +747,8 @@ class AzureAISearchContextProvider(ContextProvider):
raise ValueError("Index client is required when creating Knowledge Base from index")
if not self.azure_openai_resource_url:
raise ValueError("azure_openai_resource_url is required when creating Knowledge Base from index")
if not self.azure_openai_deployment_name:
raise ValueError("model_deployment_name is required when creating Knowledge Base from index")
if not self.azure_openai_model:
raise ValueError("model is required when creating Knowledge Base from index")
if not self.index_name:
raise ValueError("index_name is required when creating Knowledge Base from index")
@@ -782,8 +767,8 @@ class AzureAISearchContextProvider(ContextProvider):
aoai_params = AzureOpenAIVectorizerParameters(
resource_url=self.azure_openai_resource_url,
deployment_name=self.azure_openai_deployment_name,
model_name=self.model_name,
deployment_name=self.azure_openai_model,
model_name=self.azure_openai_model,
api_key=self.azure_openai_api_key,
)
@@ -155,14 +155,14 @@ class TestInitSemantic:
provider = _make_provider(context_prompt="Custom prompt:")
assert provider.context_prompt == "Custom prompt:"
def test_model_name_falls_back_to_deployment_name(self) -> None:
"""model_name defaults to model_deployment_name when not explicitly set."""
provider = _make_provider(model_deployment_name="my-deploy")
assert provider.model_name == "my-deploy"
def test_model_is_stored(self) -> None:
"""Model is stored on the provider for Azure OpenAI vectorization."""
provider = _make_provider(model="my-deploy")
assert provider.azure_openai_model == "my-deploy"
def test_model_name_explicit(self) -> None:
provider = _make_provider(model_deployment_name="deploy", model_name="gpt-4")
assert provider.model_name == "gpt-4"
def test_model_explicit(self) -> None:
provider = _make_provider(model="gpt-4")
assert provider.azure_openai_model == "gpt-4"
# -- Initialization: credential resolution ------------------------------------
@@ -214,7 +214,7 @@ class TestInitAgenticValidation:
knowledge_base_name="kb",
api_key="key",
mode="agentic",
model_deployment_name="deploy",
model="deploy",
azure_openai_resource_url="https://aoai.openai.azure.com",
)
@@ -227,8 +227,8 @@ class TestInitAgenticValidation:
mode="agentic",
)
def test_missing_model_deployment_name_raises(self) -> None:
with pytest.raises(ValueError, match="model_deployment_name"):
def test_missing_model_raises(self) -> None:
with pytest.raises(ValueError, match="model"):
AzureAISearchContextProvider(
source_id="s",
endpoint="https://test.search.windows.net",
@@ -256,7 +256,7 @@ class TestInitAgenticValidation:
index_name="idx",
api_key="key",
mode="agentic",
model_deployment_name="deploy",
model="deploy",
)
def test_agentic_with_kb_name_sets_use_existing(self) -> None:
@@ -277,7 +277,7 @@ class TestInitAgenticValidation:
index_name="idx",
api_key="key",
mode="agentic",
model_deployment_name="deploy",
model="deploy",
azure_openai_resource_url="https://aoai.openai.azure.com",
)
assert provider._use_existing_knowledge_base is False
@@ -306,7 +306,7 @@ class TestInitAgenticValidation:
index_name="idx",
api_key="key",
mode="agentic",
model_deployment_name="deploy",
model="deploy",
azure_openai_resource_url="https://aoai.openai.azure.com",
)
@@ -1011,9 +1011,9 @@ class TestEnsureKnowledgeBase:
provider.knowledge_base_name = "test-kb"
provider._index_client = AsyncMock()
provider.azure_openai_resource_url = "https://aoai.openai.azure.com"
provider.azure_openai_deployment_name = None
provider.azure_openai_model = None
with pytest.raises(ValueError, match="model_deployment_name is required"):
with pytest.raises(ValueError, match="model is required"):
await provider._ensure_knowledge_base()
async def test_missing_index_name_raises(self) -> None:
@@ -1023,7 +1023,7 @@ class TestEnsureKnowledgeBase:
provider.knowledge_base_name = "test-kb"
provider._index_client = AsyncMock()
provider.azure_openai_resource_url = "https://aoai.openai.azure.com"
provider.azure_openai_deployment_name = "deploy"
provider.azure_openai_model = "deploy"
provider.index_name = None
with pytest.raises(ValueError, match="index_name is required"):
@@ -1037,8 +1037,7 @@ class TestEnsureKnowledgeBase:
provider._use_existing_knowledge_base = False
provider.knowledge_base_name = "test-kb"
provider.azure_openai_resource_url = "https://aoai.openai.azure.com"
provider.azure_openai_deployment_name = "deploy"
provider.model_name = "gpt-4"
provider.azure_openai_model = "gpt-4"
provider.index_name = "test-index"
mock_index_client = AsyncMock()
@@ -1061,8 +1060,7 @@ class TestEnsureKnowledgeBase:
provider._use_existing_knowledge_base = False
provider.knowledge_base_name = "test-kb"
provider.azure_openai_resource_url = "https://aoai.openai.azure.com"
provider.azure_openai_deployment_name = "deploy"
provider.model_name = "gpt-4"
provider.azure_openai_model = "gpt-4"
provider.index_name = "test-index"
mock_index_client = AsyncMock()
@@ -1083,8 +1081,7 @@ class TestEnsureKnowledgeBase:
provider._use_existing_knowledge_base = False
provider.knowledge_base_name = "test-kb"
provider.azure_openai_resource_url = "https://aoai.openai.azure.com"
provider.azure_openai_deployment_name = "deploy"
provider.model_name = "gpt-4"
provider.azure_openai_model = "gpt-4"
provider.index_name = "test-index"
provider.knowledge_base_output_mode = "answer_synthesis"
@@ -1105,8 +1102,7 @@ class TestEnsureKnowledgeBase:
provider._use_existing_knowledge_base = False
provider.knowledge_base_name = "test-kb"
provider.azure_openai_resource_url = "https://aoai.openai.azure.com"
provider.azure_openai_deployment_name = "deploy"
provider.model_name = "gpt-4"
provider.azure_openai_model = "gpt-4"
provider.index_name = "test-index"
provider.retrieval_reasoning_effort = "medium"
+1 -1
View File
@@ -18,7 +18,7 @@ from agent_framework_azure_ai import AzureAIInferenceEmbeddingClient
client = AzureAIInferenceEmbeddingClient(
endpoint="https://<resource>.inference.ai.azure.com",
api_key="...",
model_id="text-embedding-3-large",
model="text-embedding-3-large",
)
result = await client.get_embeddings(["Hello"])
```
@@ -44,7 +44,7 @@ class AzureAIInferenceEmbeddingOptions(EmbeddingGenerationOptions, total=False):
from agent_framework_azure_ai import AzureAIInferenceEmbeddingOptions
options: AzureAIInferenceEmbeddingOptions = {
"model_id": "text-embedding-3-small",
"model": "text-embedding-3-small",
"dimensions": 1536,
"input_type": "document",
"encoding_format": "float",
@@ -54,8 +54,8 @@ class AzureAIInferenceEmbeddingOptions(EmbeddingGenerationOptions, total=False):
input_type: str
"""Input type hint for the model. Common values: ``"text"``, ``"query"``, ``"document"``."""
image_model_id: str
"""Override model for image embeddings. Falls back to the client's ``image_model_id``."""
image_model: str
"""Override model for image embeddings. Falls back to the client's ``image_model``."""
encoding_format: str
"""Output encoding format.
@@ -81,8 +81,8 @@ class AzureAIInferenceEmbeddingSettings(TypedDict, total=False):
endpoint: str | None
api_key: str | None
embedding_model_id: str | None
image_embedding_model_id: str | None
embedding_model: str | None
image_embedding_model: str | None
class RawAzureAIInferenceEmbeddingClient(
@@ -97,11 +97,11 @@ class RawAzureAIInferenceEmbeddingClient(
are reassembled in the original input order.
Keyword Args:
model_id: The text embedding model deployment name (e.g. "text-embedding-3-small").
Can also be set via environment variable AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID.
image_model_id: The image embedding model deployment name (e.g. "Cohere-embed-v3-english").
Can also be set via environment variable AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID.
Falls back to ``model_id`` if not provided.
model: The text embedding model (e.g. "text-embedding-3-small").
Can also be set via environment variable AZURE_AI_INFERENCE_EMBEDDING_MODEL.
image_model: The image embedding model (e.g. "Cohere-embed-v3-english").
Can also be set via environment variable AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL.
Falls back to ``model`` if not provided.
endpoint: The Azure AI Inference endpoint URL.
Can also be set via environment variable AZURE_AI_INFERENCE_ENDPOINT.
api_key: API key for authentication.
@@ -117,8 +117,8 @@ class RawAzureAIInferenceEmbeddingClient(
def __init__(
self,
*,
model_id: str | None = None,
image_model_id: str | None = None,
model: str | None = None,
image_model: str | None = None,
endpoint: str | None = None,
api_key: str | None = None,
text_client: EmbeddingsClient | None = None,
@@ -132,17 +132,17 @@ class RawAzureAIInferenceEmbeddingClient(
settings = load_settings(
AzureAIInferenceEmbeddingSettings,
env_prefix="AZURE_AI_INFERENCE_",
required_fields=["endpoint", "embedding_model_id"],
required_fields=["endpoint", "embedding_model"],
endpoint=endpoint,
api_key=api_key,
embedding_model_id=model_id,
image_embedding_model_id=image_model_id,
embedding_model=model,
image_embedding_model=image_model,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
self.model_id = settings["embedding_model_id"] # type: ignore[reportTypedDictNotRequiredAccess]
self.image_model_id: str = settings.get("image_embedding_model_id") or self.model_id # type: ignore[assignment]
self.model = settings["embedding_model"] # type: ignore[reportTypedDictNotRequiredAccess]
self.image_model: str = settings.get("image_embedding_model") or self.model # type: ignore[assignment]
resolved_endpoint = settings["endpoint"] # type: ignore[reportTypedDictNotRequiredAccess]
if credential is None and settings.get("api_key"):
@@ -202,7 +202,7 @@ class RawAzureAIInferenceEmbeddingClient(
Generated embeddings with usage metadata.
Raises:
ValueError: If model_id is not provided or an unsupported content type is encountered.
ValueError: If model is not provided or an unsupported content type is encountered.
"""
if not values:
return GeneratedEmbeddings([], options=options) # type: ignore[reportReturnType]
@@ -254,8 +254,8 @@ class RawAzureAIInferenceEmbeddingClient(
# Embed text inputs.
if text_items:
if not (text_model := opts.get("model_id") or self.model_id):
raise ValueError("An model_id is required, either in the client or options, for text inputs.")
if not (text_model := opts.get("model") or self.model):
raise ValueError("A model is required, either in the client or options, for text inputs.")
text_inputs = [t for _, t in text_items]
response = await self._text_client.embed(
input=text_inputs,
@@ -268,7 +268,7 @@ class RawAzureAIInferenceEmbeddingClient(
embeddings[original_idx] = Embedding(
vector=vector,
dimensions=len(vector),
model_id=response.model or text_model,
model=response.model or text_model,
)
if response.usage:
usage_details["input_token_count"] = (usage_details.get("input_token_count") or 0) + (
@@ -280,8 +280,8 @@ class RawAzureAIInferenceEmbeddingClient(
# Embed image inputs.
if image_items:
if not (image_model := opts.get("image_model_id") or self.image_model_id):
raise ValueError("An image_model_id is required, either in the client or options, for image inputs.")
if not (image_model := opts.get("image_model") or self.image_model):
raise ValueError("An image_model is required, either in the client or options, for image inputs.")
image_inputs = [img for _, img in image_items]
response = await self._image_client.embed(
input=image_inputs,
@@ -294,7 +294,7 @@ class RawAzureAIInferenceEmbeddingClient(
embeddings[original_idx] = Embedding(
vector=image_vector,
dimensions=len(image_vector),
model_id=response.model or image_model,
model=response.model or image_model,
)
if response.usage:
usage_details["input_token_count"] = (usage_details.get("input_token_count") or 0) + (
@@ -322,11 +322,11 @@ class AzureAIInferenceEmbeddingClient(
``Content.from_data()``.
Keyword Args:
model_id: The text embedding model deployment name (e.g. "text-embedding-3-small").
Can also be set via environment variable AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID.
image_model_id: The image embedding model deployment name
model: The text embedding model (e.g. "text-embedding-3-small").
Can also be set via environment variable AZURE_AI_INFERENCE_EMBEDDING_MODEL.
image_model: The image embedding model
(e.g. "Cohere-embed-v3-english"). Can also be set via environment variable
AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID. Falls back to ``model_id``.
AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL. Falls back to ``model``.
endpoint: The Azure AI Inference endpoint URL.
Can also be set via environment variable AZURE_AI_INFERENCE_ENDPOINT.
api_key: API key for authentication.
@@ -346,8 +346,8 @@ class AzureAIInferenceEmbeddingClient(
# Using environment variables
# Set AZURE_AI_INFERENCE_ENDPOINT=https://your-endpoint.inference.ai.azure.com
# Set AZURE_AI_INFERENCE_API_KEY=your-key
# Set AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID=text-embedding-3-small
# Set AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID=Cohere-embed-v3-english
# Set AZURE_AI_INFERENCE_EMBEDDING_MODEL=text-embedding-3-small
# Set AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL=Cohere-embed-v3-english
client = AzureAIInferenceEmbeddingClient()
# Text embeddings
@@ -368,8 +368,8 @@ class AzureAIInferenceEmbeddingClient(
def __init__(
self,
*,
model_id: str | None = None,
image_model_id: str | None = None,
model: str | None = None,
image_model: str | None = None,
endpoint: str | None = None,
api_key: str | None = None,
text_client: EmbeddingsClient | None = None,
@@ -382,8 +382,8 @@ class AzureAIInferenceEmbeddingClient(
) -> None:
"""Initialize an Azure AI Inference embedding client."""
super().__init__(
model_id=model_id,
image_model_id=image_model_id,
model=model,
image_model=image_model,
endpoint=endpoint,
api_key=api_key,
text_client=text_client,
@@ -20,8 +20,8 @@ class AzureAISettings(TypedDict, total=False):
Keyword Args:
project_endpoint: The Azure AI Project endpoint URL.
Can be set via environment variable AZURE_AI_PROJECT_ENDPOINT.
model_deployment_name: The name of the model deployment to use.
Can be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME.
model: The name of the model to use.
Can be set via environment variable AZURE_AI_MODEL.
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'.
@@ -32,12 +32,12 @@ class AzureAISettings(TypedDict, total=False):
# Using environment variables
# Set AZURE_AI_PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com
# Set AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4
# Set AZURE_AI_MODEL=gpt-4
settings = AzureAISettings()
# Or passing parameters directly
settings = AzureAISettings(
project_endpoint="https://your-project.cognitiveservices.azure.com", model_deployment_name="gpt-4"
project_endpoint="https://your-project.cognitiveservices.azure.com", model="gpt-4"
)
# Or loading from a .env file
@@ -45,4 +45,4 @@ class AzureAISettings(TypedDict, total=False):
"""
project_endpoint: str | None
model_deployment_name: str | None
model: str | None
@@ -60,7 +60,7 @@ def mock_image_client() -> AsyncMock:
def raw_client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> RawAzureAIInferenceEmbeddingClient[Any]:
"""Create a RawAzureAIInferenceEmbeddingClient with mocked SDK clients."""
return RawAzureAIInferenceEmbeddingClient(
model_id="test-model",
model="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
@@ -72,7 +72,7 @@ def raw_client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> Raw
def client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> AzureAIInferenceEmbeddingClient[Any]:
"""Create an AzureAIInferenceEmbeddingClient with mocked SDK clients."""
return AzureAIInferenceEmbeddingClient(
model_id="test-model",
model="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
@@ -162,8 +162,8 @@ class TestRawAzureAIInferenceEmbeddingClient:
async def test_model_override_in_options(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""model_id in options overrides the default."""
options: AzureAIInferenceEmbeddingOptions = {"model_id": "custom-model"}
"""model in options overrides the default."""
options: AzureAIInferenceEmbeddingOptions = {"model": "custom-model"}
await raw_client.get_embeddings(["hello"], options=options)
call_kwargs = mock_text_client.embed.call_args
@@ -196,55 +196,55 @@ class TestRawAzureAIInferenceEmbeddingClient:
{
"AZURE_AI_INFERENCE_ENDPOINT": "https://env.inference.ai.azure.com",
"AZURE_AI_INFERENCE_API_KEY": "env-key",
"AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID": "env-model",
"AZURE_AI_INFERENCE_EMBEDDING_MODEL": "env-model",
},
),
patch("agent_framework_azure_ai._embedding_client.EmbeddingsClient"),
patch("agent_framework_azure_ai._embedding_client.ImageEmbeddingsClient"),
):
client = RawAzureAIInferenceEmbeddingClient()
assert client.model_id == "env-model"
assert client.image_model_id == "env-model" # falls back to model_id
assert client.model == "env-model"
assert client.image_model == "env-model" # falls back to model
def test_image_model_id_from_env(self) -> None:
"""image_model_id is loaded from its own environment variable."""
def test_image_model_from_env(self) -> None:
"""image_model is loaded from its own environment variable."""
with (
patch.dict(
os.environ,
{
"AZURE_AI_INFERENCE_ENDPOINT": "https://env.inference.ai.azure.com",
"AZURE_AI_INFERENCE_API_KEY": "env-key",
"AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID": "text-model",
"AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID": "image-model",
"AZURE_AI_INFERENCE_EMBEDDING_MODEL": "text-model",
"AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL": "image-model",
},
),
patch("agent_framework_azure_ai._embedding_client.EmbeddingsClient"),
patch("agent_framework_azure_ai._embedding_client.ImageEmbeddingsClient"),
):
client = RawAzureAIInferenceEmbeddingClient()
assert client.model_id == "text-model"
assert client.image_model_id == "image-model"
assert client.model == "text-model"
assert client.image_model == "image-model"
def test_image_model_id_explicit(self, mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> None:
"""image_model_id can be set explicitly."""
def test_image_model_explicit(self, mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> None:
"""image_model can be set explicitly."""
client = RawAzureAIInferenceEmbeddingClient(
model_id="text-model",
image_model_id="image-model",
model="text-model",
image_model="image-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
image_client=mock_image_client,
)
assert client.model_id == "text-model"
assert client.image_model_id == "image-model"
assert client.model == "text-model"
assert client.image_model == "image-model"
async def test_image_model_id_sent_to_image_client(
async def test_image_model_sent_to_image_client(
self, mock_text_client: AsyncMock, mock_image_client: AsyncMock
) -> None:
"""image_model_id is passed to the image client embed call."""
"""image_model is passed to the image client embed call."""
client = RawAzureAIInferenceEmbeddingClient(
model_id="text-model",
image_model_id="image-model",
model="text-model",
image_model="image-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
@@ -274,7 +274,7 @@ class TestAzureAIInferenceEmbeddingClient:
async def test_otel_provider_name_override(self, mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> None:
"""OTEL provider name can be overridden."""
client = AzureAIInferenceEmbeddingClient(
model_id="test-model",
model="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
@@ -291,7 +291,7 @@ def _integration_tests_enabled() -> bool:
return bool(
os.environ.get("AZURE_AI_INFERENCE_ENDPOINT")
and os.environ.get("AZURE_AI_INFERENCE_API_KEY")
and os.environ.get("AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID")
and os.environ.get("AZURE_AI_INFERENCE_EMBEDDING_MODEL")
)
@@ -313,4 +313,4 @@ class TestAzureAIInferenceEmbeddingIntegration:
result = await client.get_embeddings(["Hello, world!"])
assert len(result) == 1
assert len(result[0].vector) > 0
assert result[0].model_id is not None
assert result[0].model is not None
+1 -1
View File
@@ -29,7 +29,7 @@ def azure_ai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
env_vars = {
"AZURE_AI_PROJECT_ENDPOINT": "https://test-project.cognitiveservices.azure.com/",
"AZURE_AI_MODEL_DEPLOYMENT_NAME": "test-gpt-4o",
"AZURE_AI_MODEL": "test-gpt-4o",
}
env_vars.update(override_env_param_dict) # type: ignore
@@ -35,7 +35,7 @@ Optional:
async def main() -> None:
"""Run the Cosmos history provider sample with an Agent."""
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
deployment_name = os.getenv("FOUNDRY_MODEL")
model = os.getenv("FOUNDRY_MODEL")
cosmos_endpoint = os.getenv("AZURE_COSMOS_ENDPOINT")
cosmos_database_name = os.getenv("AZURE_COSMOS_DATABASE_NAME")
cosmos_container_name = os.getenv("AZURE_COSMOS_CONTAINER_NAME")
@@ -43,7 +43,7 @@ async def main() -> None:
if (
not project_endpoint
or not deployment_name
or not model
or not cosmos_endpoint
or not cosmos_database_name
or not cosmos_container_name
@@ -58,7 +58,7 @@ async def main() -> None:
async with AzureCliCredential() as credential:
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=deployment_name,
model=model,
credential=credential,
)
@@ -1,6 +1,6 @@
# Azure OpenAI Configuration
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT_NAME=your-deployment-name
AZURE_OPENAI_MODEL=your-deployment-name
FUNCTIONS_WORKER_RUNTIME=python
# Azure Functions Configuration
@@ -14,7 +14,7 @@ cp .env.example .env
Required variables:
- `AZURE_OPENAI_ENDPOINT`
- `AZURE_OPENAI_DEPLOYMENT_NAME`
- `AZURE_OPENAI_MODEL`
- `AZURE_OPENAI_API_KEY`
- `AzureWebJobsStorage`
- `DURABLE_TASK_SCHEDULER_CONNECTION_STRING`
@@ -115,7 +115,7 @@ def _should_skip_azure_functions_integration_tests() -> tuple[bool, str]:
os.getenv("FOUNDRY_MODEL", "").strip()
)
has_azure_openai_config = bool(os.getenv("AZURE_OPENAI_ENDPOINT", "").strip()) and bool(
os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "").strip()
os.getenv("AZURE_OPENAI_MODEL", "").strip()
)
if not has_foundry_config and not has_azure_openai_config:
return (
@@ -339,7 +339,7 @@ def _load_and_validate_env(sample_path: Path) -> None:
"FUNCTIONS_WORKER_RUNTIME",
]
if sample_path.name == "11_workflow_parallel":
required_env_vars.extend(["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT_NAME"])
required_env_vars.extend(["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_MODEL"])
else:
required_env_vars.extend(["FOUNDRY_PROJECT_ENDPOINT", "FOUNDRY_MODEL"])
+1 -1
View File
@@ -14,7 +14,7 @@ Integration with AWS Bedrock for LLM inference.
```python
from agent_framework.amazon import BedrockChatClient
client = BedrockChatClient(model_id="anthropic.claude-3-sonnet-20240229-v1:0")
client = BedrockChatClient(model="anthropic.claude-3-sonnet-20240229-v1:0")
response = await client.get_response("Hello")
```
@@ -101,7 +101,7 @@ class BedrockChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], t
Keys:
# Inherited from ChatOptions (mapped to Bedrock):
model_id: The Bedrock model identifier,
model: The Bedrock model identifier,
translates to ``modelId`` in Bedrock API.
temperature: Sampling temperature,
translates to ``inferenceConfig.temperature``.
@@ -175,7 +175,7 @@ class BedrockChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], t
BEDROCK_OPTION_TRANSLATIONS: dict[str, str] = {
"model_id": "modelId",
"model": "modelId",
"max_tokens": "maxTokens",
"top_p": "topP",
"stop": "stopSequences",
@@ -209,7 +209,7 @@ class BedrockSettings(TypedDict, total=False):
"""Bedrock configuration settings pulled from environment variables or .env files."""
region: str | None
chat_model_id: str | None
chat_model: str | None
access_key: SecretString | None
secret_key: SecretString | None
session_token: SecretString | None
@@ -230,7 +230,7 @@ class BedrockChatClient(
self,
*,
region: str | None = None,
model_id: str | None = None,
model: str | None = None,
access_key: str | None = None,
secret_key: str | None = None,
session_token: str | None = None,
@@ -246,7 +246,7 @@ class BedrockChatClient(
Args:
region: Region to send Bedrock requests to; falls back to BEDROCK_REGION.
model_id: Default model identifier; falls back to BEDROCK_CHAT_MODEL_ID.
model: Default model identifier; falls back to BEDROCK_CHAT_MODEL.
access_key: Optional AWS access key for manual credential injection.
secret_key: Optional AWS secret key paired with ``access_key``.
session_token: Optional AWS session token for temporary credentials.
@@ -264,7 +264,7 @@ class BedrockChatClient(
from agent_framework.amazon import BedrockChatClient
# Basic usage with default credentials
client = BedrockChatClient(model_id="<model name>")
client = BedrockChatClient(model="<model name>")
# Using custom ChatOptions with type safety:
from typing import TypedDict
@@ -275,14 +275,14 @@ class BedrockChatClient(
my_custom_option: str
client = BedrockChatClient[MyOptions](model_id="<model name>")
client = BedrockChatClient[MyOptions](model="<model name>")
response = await client.get_response("Hello", options={"my_custom_option": "value"})
"""
settings = load_settings(
BedrockSettings,
env_prefix="BEDROCK_",
region=region,
chat_model_id=model_id,
chat_model=model,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
@@ -290,7 +290,7 @@ class BedrockChatClient(
env_file_encoding=env_file_encoding,
)
region = settings.get("region") or DEFAULT_REGION
chat_model_id = settings.get("chat_model_id")
chat_model = settings.get("chat_model")
if client:
self._bedrock_client = client
@@ -307,7 +307,7 @@ class BedrockChatClient(
middleware=middleware,
function_invocation_configuration=function_invocation_configuration,
)
self.model_id = chat_model_id
self.model = chat_model
self.region = region
@staticmethod
@@ -355,7 +355,7 @@ class BedrockChatClient(
yield ChatResponseUpdate(
response_id=parsed_response.response_id,
contents=contents,
model_id=parsed_response.model_id,
model=parsed_response.model,
finish_reason=finish_reason,
raw_representation=parsed_response.raw_representation,
)
@@ -375,10 +375,10 @@ class BedrockChatClient(
options: Mapping[str, Any],
**kwargs: Any,
) -> dict[str, Any]:
model_id = options.get("model_id") or self.model_id
if not model_id:
model = options.get("model") or self.model
if not model:
raise ValueError(
"Bedrock model_id is required. Set via chat options or BEDROCK_CHAT_MODEL_ID environment variable."
"Bedrock model is required. Set via chat options or BEDROCK_CHAT_MODEL environment variable."
)
system_prompts, conversation = self._prepare_bedrock_messages(messages)
@@ -389,7 +389,7 @@ class BedrockChatClient(
system_prompts = [{"text": instructions}, *system_prompts]
run_options: dict[str, Any] = {
"modelId": model_id,
"modelId": model,
"messages": conversation,
"inferenceConfig": {"maxTokens": options.get("max_tokens", DEFAULT_MAX_TOKENS)},
}
@@ -633,12 +633,12 @@ class BedrockChatClient(
usage_details = self._parse_usage(usage_source)
finish_reason = self._map_finish_reason(output.get("completionReason") or response.get("stopReason"))
response_id = response.get("responseId") or message.get("id")
model_id = response.get("modelId") or output.get("modelId") or self.model_id
model = response.get("modelId") or output.get("modelId") or self.model
return ChatResponse(
response_id=response_id,
messages=[chat_message],
usage_details=usage_details,
model_id=model_id,
model=model,
finish_reason=finish_reason,
raw_representation=response,
)
@@ -39,7 +39,7 @@ class BedrockEmbeddingSettings(TypedDict, total=False):
"""Bedrock embedding settings."""
region: str | None
embedding_model_id: str | None
embedding_model: str | None
access_key: SecretString | None
secret_key: SecretString | None
session_token: SecretString | None
@@ -56,7 +56,7 @@ class BedrockEmbeddingOptions(EmbeddingGenerationOptions, total=False):
from agent_framework_bedrock import BedrockEmbeddingOptions
options: BedrockEmbeddingOptions = {
"model_id": "amazon.titan-embed-text-v2:0",
"model": "amazon.titan-embed-text-v2:0",
"dimensions": 1024,
"normalize": True,
}
@@ -80,8 +80,8 @@ class RawBedrockEmbeddingClient(
"""Raw Bedrock embedding client without telemetry.
Keyword Args:
model_id: The Bedrock embedding model ID (e.g. "amazon.titan-embed-text-v2:0").
Can also be set via environment variable BEDROCK_EMBEDDING_MODEL_ID.
model: The Bedrock embedding model ID (e.g. "amazon.titan-embed-text-v2:0").
Can also be set via environment variable BEDROCK_EMBEDDING_MODEL.
region: AWS region. Will try to load from BEDROCK_REGION env var,
if not set, the regular Boto3 configuration/loading applies
(which may include other env vars, config files, or instance metadata).
@@ -98,7 +98,7 @@ class RawBedrockEmbeddingClient(
self,
*,
region: str | None = None,
model_id: str | None = None,
model: str | None = None,
access_key: str | None = None,
secret_key: str | None = None,
session_token: str | None = None,
@@ -112,9 +112,9 @@ class RawBedrockEmbeddingClient(
settings = load_settings(
BedrockEmbeddingSettings,
env_prefix="BEDROCK_",
required_fields=["embedding_model_id"],
required_fields=["embedding_model"],
region=region,
embedding_model_id=model_id,
embedding_model=model,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
@@ -143,7 +143,7 @@ class RawBedrockEmbeddingClient(
config=BotoConfig(user_agent_extra=AGENT_FRAMEWORK_USER_AGENT),
)
self.model_id: str = settings["embedding_model_id"] # type: ignore[assignment] # pyright: ignore[reportTypedDictNotRequiredAccess]
self.model: str = settings["embedding_model"] # type: ignore[assignment] # pyright: ignore[reportTypedDictNotRequiredAccess]
self.region = resolved_region
super().__init__(additional_properties=additional_properties)
@@ -170,15 +170,15 @@ class RawBedrockEmbeddingClient(
Generated embeddings with usage metadata.
Raises:
ValueError: If model_id is not provided or values is empty.
ValueError: If model is not provided or values is empty.
"""
if not values:
return GeneratedEmbeddings([], options=options)
opts: dict[str, Any] = dict(options) if options else {}
model = opts.get("model_id") or self.model_id
model = opts.get("model") or self.model
if not model:
raise ValueError("model_id is required")
raise ValueError("model is required")
embedding_results = await asyncio.gather(
*(self._generate_embedding_for_text(opts, model, text) for text in values)
@@ -218,7 +218,7 @@ class RawBedrockEmbeddingClient(
embedding = Embedding(
vector=response_body["embedding"],
dimensions=len(response_body["embedding"]),
model_id=model,
model=model,
)
input_tokens = int(response_body.get("inputTextTokenCount", 0))
return embedding, input_tokens
@@ -234,8 +234,8 @@ class BedrockEmbeddingClient(
Uses the Amazon Titan Embeddings model via Bedrock's invoke_model API.
Keyword Args:
model_id: The Bedrock embedding model ID (e.g. "amazon.titan-embed-text-v2:0").
Can also be set via environment variable BEDROCK_EMBEDDING_MODEL_ID.
model: The Bedrock embedding model ID (e.g. "amazon.titan-embed-text-v2:0").
Can also be set via environment variable BEDROCK_EMBEDDING_MODEL.
region: AWS region. Defaults to "us-east-1".
Can also be set via environment variable BEDROCK_REGION.
access_key: AWS access key for manual credential injection.
@@ -253,7 +253,7 @@ class BedrockEmbeddingClient(
# Using default AWS credentials
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
model="amazon.titan-embed-text-v2:0",
)
# Generate embeddings
@@ -267,7 +267,7 @@ class BedrockEmbeddingClient(
self,
*,
region: str | None = None,
model_id: str | None = None,
model: str | None = None,
access_key: str | None = None,
secret_key: str | None = None,
session_token: str | None = None,
@@ -281,7 +281,7 @@ class BedrockEmbeddingClient(
"""Initialize a Bedrock embedding client."""
super().__init__(
region=region,
model_id=model_id,
model=model,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
@@ -39,17 +39,17 @@ async def test_bedrock_embedding_construction() -> None:
"""Test construction with explicit parameters."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
model="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
assert client.model_id == "amazon.titan-embed-text-v2:0"
assert client.model == "amazon.titan-embed-text-v2:0"
assert client.region == "us-west-2"
async def test_bedrock_embedding_construction_missing_model_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that missing model_id raises an error."""
monkeypatch.delenv("BEDROCK_EMBEDDING_MODEL_ID", raising=False)
"""Test that missing model raises an error."""
monkeypatch.delenv("BEDROCK_EMBEDDING_MODEL", raising=False)
from agent_framework.exceptions import SettingNotFoundError
with pytest.raises(SettingNotFoundError):
@@ -60,7 +60,7 @@ async def test_bedrock_embedding_get_embeddings() -> None:
"""Test generating embeddings via the Bedrock invoke_model API."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
model="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
@@ -71,7 +71,7 @@ async def test_bedrock_embedding_get_embeddings() -> None:
assert len(result) == 2
assert len(result[0].vector) == 3
assert len(result[1].vector) == 3
assert result[0].model_id == "amazon.titan-embed-text-v2:0"
assert result[0].model == "amazon.titan-embed-text-v2:0"
assert result.usage == {"input_token_count": 10}
# Two calls since Titan processes one input at a time
@@ -84,7 +84,7 @@ async def test_bedrock_embedding_get_embeddings_empty_input() -> None:
"""Test generating embeddings with empty input."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
model="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
@@ -100,7 +100,7 @@ async def test_bedrock_embedding_get_embeddings_with_options() -> None:
"""Test generating embeddings with custom options."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
model="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
@@ -120,16 +120,16 @@ async def test_bedrock_embedding_get_embeddings_with_options() -> None:
async def test_bedrock_embedding_get_embeddings_no_model_raises() -> None:
"""Test that missing model_id at call time raises ValueError."""
"""Test that missing model at call time raises ValueError."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
model="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
client.model_id = None # type: ignore[assignment]
client.model = None # type: ignore[assignment]
with pytest.raises(ValueError, match="model_id is required"):
with pytest.raises(ValueError, match="model is required"):
await client.get_embeddings(["hello"])
@@ -137,7 +137,7 @@ async def test_bedrock_embedding_default_region() -> None:
"""Test that default region is us-east-1."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
model="amazon.titan-embed-text-v2:0",
client=stub,
)
assert client.region == "us-east-1"
@@ -146,7 +146,7 @@ async def test_bedrock_embedding_default_region() -> None:
# region: Integration Tests
skip_if_bedrock_embedding_integration_tests_disabled = pytest.mark.skipif(
os.getenv("BEDROCK_EMBEDDING_MODEL_ID", "") in ("", "test-model")
os.getenv("BEDROCK_EMBEDDING_MODEL", "") in ("", "test-model")
or not (os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("BEDROCK_ACCESS_KEY")),
reason="No real Bedrock embedding model or AWS credentials provided; skipping integration tests.",
)
@@ -34,7 +34,7 @@ class _StubBedrockRuntime:
def _make_client() -> BedrockChatClient:
"""Create a BedrockChatClient with a stub runtime for unit tests."""
return BedrockChatClient(
model_id="amazon.titan-text",
model="amazon.titan-text",
region="us-west-2",
client=_StubBedrockRuntime(),
)
@@ -43,7 +43,7 @@ def _make_client() -> BedrockChatClient:
async def test_get_response_invokes_bedrock_runtime() -> None:
stub = _StubBedrockRuntime()
client = BedrockChatClient(
model_id="amazon.titan-text",
model="amazon.titan-text",
region="us-west-2",
client=stub,
)
@@ -65,7 +65,7 @@ async def test_get_response_invokes_bedrock_runtime() -> None:
def test_build_request_requires_non_system_messages() -> None:
client = BedrockChatClient(
model_id="amazon.titan-text",
model="amazon.titan-text",
region="us-west-2",
client=_StubBedrockRuntime(),
)
@@ -24,7 +24,7 @@ class _WeatherArgs(BaseModel):
def _build_client() -> BedrockChatClient:
fake_runtime = MagicMock()
fake_runtime.converse.return_value = {}
return BedrockChatClient(model_id="test-model", client=fake_runtime)
return BedrockChatClient(model="test-model", client=fake_runtime)
def _dummy_weather(location: str) -> str: # pragma: no cover - helper
@@ -33,10 +33,10 @@ def _dummy_weather(location: str) -> str: # pragma: no cover - helper
def test_settings_load_from_environment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("BEDROCK_REGION", "us-west-2")
monkeypatch.setenv("BEDROCK_CHAT_MODEL_ID", "anthropic.claude-v2")
monkeypatch.setenv("BEDROCK_CHAT_MODEL", "anthropic.claude-v2")
settings = load_settings(BedrockSettings, env_prefix="BEDROCK_")
assert settings["region"] == "us-west-2"
assert settings["chat_model_id"] == "anthropic.claude-v2"
assert settings["chat_model"] == "anthropic.claude-v2"
def test_build_request_includes_tool_config() -> None:
+1 -1
View File
@@ -39,7 +39,7 @@ OPENAI_RESPONSES_MODEL=...
...
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_ENDPOINT=...
AZURE_OPENAI_DEPLOYMENT_NAME=...
AZURE_OPENAI_MODEL=...
```
You can also override environment variables by explicitly passing configuration parameters to the chat client constructor:
@@ -100,6 +100,7 @@ def _merge_options(base: dict[str, Any], override: dict[str, Any]) -> dict[str,
A new merged options dict.
"""
result = dict(base)
for key, value in override.items():
if value is None:
continue
@@ -596,7 +597,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
from agent_framework.openai import OpenAIChatClient
# Create a basic chat agent
client = OpenAIChatClient(model_id="gpt-4")
client = OpenAIChatClient(model="gpt-4")
agent = Agent(client=client, name="assistant", description="A helpful assistant")
# Run the agent with a simple message
@@ -634,7 +635,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions
client = OpenAIChatClient(model_id="gpt-4o")
client = OpenAIChatClient(model="gpt-4o")
agent: Agent[OpenAIChatOptions] = Agent(
client=client,
name="reasoning-agent",
@@ -692,7 +693,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
service-managed conversation instead.
default_options: A TypedDict containing chat options. When using a typed agent like
``Agent[OpenAIChatOptions]``, this enables IDE autocomplete for
provider-specific options including temperature, max_tokens, model_id,
provider-specific options including temperature, max_tokens, model,
tool_choice, and provider-specific options like reasoning_effort.
You can also create your own TypedDict for custom chat clients.
Note: response_format typing does not flow into run outputs when set via default_options.
@@ -736,9 +737,10 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)]
agent_tools = [tool for tool in normalized_tools if not isinstance(tool, MCPTool)]
model = opts.pop("model", None) or getattr(self.client, "model", None)
# Build chat options dict
self.default_options: dict[str, Any] = {
"model_id": opts.pop("model_id", None) or (getattr(self.client, "model_id", None)),
"allow_multiple_tool_calls": opts.pop("allow_multiple_tool_calls", None),
"conversation_id": opts.pop("conversation_id", None),
"frequency_penalty": opts.pop("frequency_penalty", None),
@@ -758,6 +760,8 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
"user": opts.pop("user", None),
**opts, # Remaining options are provider-specific
}
if model is not None:
self.default_options["model"] = model
# Remove None values from chat_options
self.default_options = {k: v for k, v in self.default_options.items() if v is not None}
self._async_exit_stack = AsyncExitStack()
@@ -914,7 +918,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
tools: The tools to use for this specific run (merged with default tools).
options: A TypedDict containing chat options. When using a typed agent like
``Agent[OpenAIChatOptions]``, this enables IDE autocomplete for
provider-specific options including temperature, max_tokens, model_id,
provider-specific options including temperature, max_tokens, model,
tool_choice, and provider-specific options like reasoning_effort.
compaction_strategy: Optional per-run compaction override passed to
``client.get_response()``. When omitted, the agent-level override
@@ -1243,9 +1247,10 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
)
additional_function_arguments = {**effective_function_invocation_kwargs, **existing_additional_args}
model = opts.pop("model", None)
# Build options dict from run() options merged with provided options
run_opts: dict[str, Any] = {
"model_id": opts.pop("model_id", None),
"conversation_id": active_session.service_session_id
if active_session
else opts.pop("conversation_id", None),
@@ -1266,6 +1271,8 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
"user": opts.pop("user", None),
**opts, # Remaining options are provider-specific
}
if model is not None:
run_opts["model"] = model
# Remove None values and merge with chat_options
run_opts = {k: v for k, v in run_opts.items() if v is not None}
co = _merge_options(chat_options, run_opts)
@@ -592,7 +592,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
tools: The tools to use for the request.
default_options: A TypedDict containing chat options. When using a typed client like
``OpenAIChatClient``, this enables IDE autocomplete for provider-specific options
including temperature, max_tokens, model_id, tool_choice, and more.
including temperature, max_tokens, model, tool_choice, and more.
Note: response_format typing does not flow into run outputs when set via default_options,
and dict literals are accepted without specialized option typing.
context_providers: Context providers to include during agent invocation.
@@ -617,7 +617,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
from agent_framework.openai import OpenAIChatClient
# Create a client
client = OpenAIChatClient(model_id="gpt-4")
client = OpenAIChatClient(model="gpt-4")
# Create an agent using the convenience method
agent = client.as_agent(
+1 -1
View File
@@ -819,7 +819,7 @@ class MCPTool:
return types.CreateMessageResult(
role="assistant",
content=mcp_content,
model=response.model_id or "unknown",
model=response.model or "unknown",
)
async def logging_callback(self, params: types.LoggingMessageNotificationParams) -> None:
@@ -294,7 +294,7 @@ class ChatContext:
async def process(self, context: ChatContext, call_next):
print(f"Chat client: {context.chat_client.__class__.__name__}")
print(f"Messages: {len(context.messages)}")
print(f"Model: {context.options.get('model_id')}")
print(f"Model: {context.options.get('model')}")
# Store metadata
context.metadata["input_tokens"] = self.count_tokens(context.messages)
@@ -431,7 +431,7 @@ class SerializationMixin:
# Serialized data contains only the model configuration
client_data = {
"type": "open_ai_chat_client",
"model_id": "gpt-4o-mini",
"model": "gpt-4o-mini",
# client is excluded from serialization
}
@@ -11,21 +11,21 @@ Usage::
class MySettings(TypedDict, total=False):
api_key: str | None # optional — resolves to None if not set
model_id: str | None # optional by default
model: str | None # optional by default
source_a: str | None
source_b: str | None
# Make model_id required; require exactly one of source_a / source_b:
# Make model required; require exactly one of source_a / source_b:
settings = load_settings(
MySettings,
env_prefix="MY_APP_",
required_fields=["model_id", ("source_a", "source_b")],
model_id="gpt-4",
required_fields=["model", ("source_a", "source_b")],
model="gpt-4",
source_a="value",
)
settings["api_key"] # type-checked dict access
settings["model_id"] # str | None per type, but guaranteed not None at runtime
settings["model"] # str | None per type, but guaranteed not None at runtime
"""
from __future__ import annotations
+15 -54
View File
@@ -1872,8 +1872,8 @@ def _process_update(response: ChatResponse | AgentResponse, update: ChatResponse
response.conversation_id = update.conversation_id
if update.finish_reason is not None:
response.finish_reason = update.finish_reason
if update.model_id is not None:
response.model_id = update.model_id
if update.model is not None:
response.model = update.model
response.continuation_token = update.continuation_token
@@ -1956,7 +1956,7 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
messages: The list of chat messages in the response.
response_id: The ID of the chat response.
conversation_id: An identifier for the state of the conversation.
model_id: The model ID used in the creation of the chat response.
model: The model used in the creation of the chat response.
created_at: A timestamp for the chat response.
finish_reason: The reason for the chat response.
usage_details: The usage details for the chat response.
@@ -1979,7 +1979,7 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
response = ChatResponse(
messages=[msg],
finish_reason="stop",
model_id="gpt-4",
model="gpt-4",
)
print(response.text) # "The weather is sunny."
@@ -1989,13 +1989,13 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
# Serialization - to_dict and from_dict
response_dict = response.to_dict()
# {'type': 'chat_response', 'messages': [...], 'model_id': 'gpt-4', 'finish_reason': 'stop'}
# {'type': 'chat_response', 'messages': [...], 'model': 'gpt-4', 'finish_reason': 'stop'}
restored_response = ChatResponse.from_dict(response_dict)
print(restored_response.model_id) # "gpt-4"
print(restored_response.model) # "gpt-4"
# Serialization - to_json and from_json
response_json = response.to_json()
# '{"type": "chat_response", "messages": [...], "model_id": "gpt-4", ...}'
# '{"type": "chat_response", "messages": [...], "model": "gpt-4", ...}'
restored_from_json = ChatResponse.from_json(response_json)
print(restored_from_json.text) # "The weather is sunny."
"""
@@ -2010,7 +2010,6 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
response_id: str | None = None,
conversation_id: str | None = None,
model: str | None = None,
model_id: str | None = None,
created_at: CreatedAtT | None = None,
finish_reason: FinishReasonLiteral | FinishReason | None = None,
usage_details: UsageDetails | None = None,
@@ -2027,7 +2026,6 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
response_id: Optional ID of the chat response.
conversation_id: Optional identifier for the state of the conversation.
model: Optional model used in the creation of the chat response.
model_id: Deprecated alias for ``model``.
created_at: Optional timestamp for when the response was created.
finish_reason: Optional reason for the chat response (e.g., "stop", "length", "tool_calls").
usage_details: Optional usage details for the chat response.
@@ -2038,8 +2036,6 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
additional_properties: Optional additional properties associated with the chat response.
raw_representation: Optional raw representation of the chat response from an underlying implementation.
"""
if model_id is not None and model is None:
model = model_id
if messages is None:
self.messages: list[Message] = []
elif isinstance(messages, Message):
@@ -2082,15 +2078,6 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]):
"""Return whether conversation_id is internal control-flow state."""
return bool(self.additional_properties.get(self._INTERNAL_CONVERSATION_ID_KEY, False))
@property
def model_id(self) -> str | None:
"""Deprecated alias for :attr:`model`."""
return self.model
@model_id.setter
def model_id(self, value: str | None) -> None:
self.model = value
@overload
@classmethod
def from_updates(
@@ -2243,7 +2230,7 @@ class ChatResponseUpdate(SerializationMixin):
response_id: The ID of the response of which this update is a part.
message_id: The ID of the message of which this update is a part.
conversation_id: An identifier for the state of the conversation of which this update is a part.
model_id: The model ID associated with this response update.
model: The model associated with this response update.
created_at: A timestamp for the chat response update.
finish_reason: The finish reason for the operation.
additional_properties: Any additional properties associated with the chat response update.
@@ -2289,7 +2276,6 @@ class ChatResponseUpdate(SerializationMixin):
message_id: str | None = None,
conversation_id: str | None = None,
model: str | None = None,
model_id: str | None = None,
created_at: CreatedAtT | None = None,
finish_reason: FinishReasonLiteral | FinishReason | None = None,
continuation_token: ContinuationToken | None = None,
@@ -2306,7 +2292,6 @@ class ChatResponseUpdate(SerializationMixin):
message_id: Optional ID of the message of which this update is a part.
conversation_id: Optional identifier for the state of the conversation of which this update is a part
model: Optional model associated with this response update.
model_id: Deprecated alias for ``model``.
created_at: Optional timestamp for the chat response update.
finish_reason: Optional finish reason for the operation.
continuation_token: Optional token for resuming a long-running background operation.
@@ -2316,8 +2301,6 @@ class ChatResponseUpdate(SerializationMixin):
from an underlying implementation.
"""
if model_id is not None and model is None:
model = model_id
# Handle contents - support dict conversion for from_dict
if contents is None:
self.contents: list[Content] = []
@@ -2347,15 +2330,6 @@ class ChatResponseUpdate(SerializationMixin):
)
self.raw_representation = raw_representation
@property
def model_id(self) -> str | None:
"""Deprecated alias for :attr:`model`."""
return self.model
@model_id.setter
def model_id(self, value: str | None) -> None:
self.model = value
@property
def text(self) -> str:
"""Returns the concatenated text of all contents in the update."""
@@ -3121,12 +3095,12 @@ class _ChatOptionsBase(TypedDict, total=False):
options: ChatOptions = {
"temperature": 0.7,
"max_tokens": 1000,
"model_id": "gpt-4",
"model": "gpt-4",
}
# With tools
options_with_tools: ChatOptions = {
"model_id": "gpt-4",
"model": "gpt-4",
"tool_choice": "auto",
"temperature": 0.7,
}
@@ -3136,8 +3110,7 @@ class _ChatOptionsBase(TypedDict, total=False):
"""
# Model selection
model_id: str
model: str
# Generation parameters
temperature: float
top_p: float
@@ -3373,10 +3346,10 @@ def merge_chat_options(
from agent_framework import merge_chat_options
base = {"temperature": 0.5, "model_id": "gpt-4"}
base = {"temperature": 0.5, "model": "gpt-4"}
override = {"temperature": 0.7, "max_tokens": 1000}
merged = merge_chat_options(base, override)
# {"temperature": 0.7, "model_id": "gpt-4", "max_tokens": 1000}
# {"temperature": 0.7, "model": "gpt-4", "max_tokens": 1000}
"""
if not base:
return dict(override) if override else {}
@@ -3453,12 +3426,12 @@ class EmbeddingGenerationOptions(TypedDict, total=False):
from agent_framework import EmbeddingGenerationOptions
options: EmbeddingGenerationOptions = {
"model_id": "text-embedding-3-small",
"model": "text-embedding-3-small",
"dimensions": 1536,
}
"""
model_id: str
model: str
dimensions: int
@@ -3492,13 +3465,10 @@ class Embedding(Generic[EmbeddingT]):
vector: EmbeddingT,
*,
model: str | None = None,
model_id: str | None = None,
dimensions: int | None = None,
created_at: datetime | None = None,
additional_properties: dict[str, Any] | None = None,
) -> None:
if model_id is not None and model is None:
model = model_id
self.vector = vector
self._dimensions = dimensions
self.model = model
@@ -3507,15 +3477,6 @@ class Embedding(Generic[EmbeddingT]):
_restore_compaction_annotation_in_additional_properties(additional_properties) or {}
)
@property
def model_id(self) -> str | None:
"""Deprecated alias for :attr:`model`."""
return self.model
@model_id.setter
def model_id(self, value: str | None) -> None:
self.model = value
@property
def dimensions(self) -> int | None:
"""Return the number of dimensions in the embedding vector.
@@ -3,9 +3,11 @@
"""Amazon Bedrock integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-anthropic``
- ``agent-framework-bedrock``
Supported classes:
- AnthropicBedrockClient
- BedrockChatClient
- BedrockChatOptions
- BedrockEmbeddingClient
@@ -13,34 +15,36 @@ Supported classes:
- BedrockEmbeddingSettings
- BedrockGuardrailConfig
- BedrockSettings
- RawAnthropicBedrockClient
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_bedrock"
PACKAGE_NAME = "agent-framework-bedrock"
_IMPORTS = [
"BedrockChatClient",
"BedrockChatOptions",
"BedrockEmbeddingClient",
"BedrockEmbeddingOptions",
"BedrockEmbeddingSettings",
"BedrockGuardrailConfig",
"BedrockSettings",
]
_IMPORTS: dict[str, tuple[str, str]] = {
"AnthropicBedrockClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"BedrockChatClient": ("agent_framework_bedrock", "agent-framework-bedrock"),
"BedrockChatOptions": ("agent_framework_bedrock", "agent-framework-bedrock"),
"BedrockEmbeddingClient": ("agent_framework_bedrock", "agent-framework-bedrock"),
"BedrockEmbeddingOptions": ("agent_framework_bedrock", "agent-framework-bedrock"),
"BedrockEmbeddingSettings": ("agent_framework_bedrock", "agent-framework-bedrock"),
"BedrockGuardrailConfig": ("agent_framework_bedrock", "agent-framework-bedrock"),
"BedrockSettings": ("agent_framework_bedrock", "agent-framework-bedrock"),
"RawAnthropicBedrockClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
}
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
import_path, package_name = _IMPORTS[name]
try:
return getattr(importlib.import_module(IMPORT_PATH), name)
return getattr(importlib.import_module(import_path), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
f"The '{package_name}' package is not installed, please do `pip install {package_name}`"
) from exc
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
raise AttributeError(f"Module `amazon` has no attribute {name}.")
def __dir__() -> list[str]:
return _IMPORTS
return list(_IMPORTS.keys())
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_anthropic import AnthropicBedrockClient, RawAnthropicBedrockClient
from agent_framework_bedrock import (
BedrockChatClient,
BedrockChatOptions,
@@ -11,6 +12,7 @@ from agent_framework_bedrock import (
)
__all__ = [
"AnthropicBedrockClient",
"BedrockChatClient",
"BedrockChatOptions",
"BedrockEmbeddingClient",
@@ -18,4 +20,5 @@ __all__ = [
"BedrockEmbeddingSettings",
"BedrockGuardrailConfig",
"BedrockSettings",
"RawAnthropicBedrockClient",
]
@@ -7,22 +7,36 @@ This module lazily re-exports objects from:
- ``agent-framework-claude``
Supported classes:
- AnthropicBedrockClient
- AnthropicClient
- AnthropicChatOptions
- AnthropicFoundryClient
- AnthropicVertexClient
- ClaudeAgent
- ClaudeAgentOptions
- RawAnthropicBedrockClient
- RawAnthropicClient
- RawAnthropicFoundryClient
- RawClaudeAgent
- RawAnthropicVertexClient
"""
import importlib
from typing import Any
_IMPORTS: dict[str, tuple[str, str]] = {
"AnthropicBedrockClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"AnthropicClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"AnthropicChatOptions": ("agent_framework_anthropic", "agent-framework-anthropic"),
"AnthropicFoundryClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"AnthropicVertexClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"ClaudeAgent": ("agent_framework_claude", "agent-framework-claude"),
"ClaudeAgentOptions": ("agent_framework_claude", "agent-framework-claude"),
"RawAnthropicBedrockClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"RawAnthropicClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"RawAnthropicFoundryClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"RawClaudeAgent": ("agent_framework_claude", "agent-framework-claude"),
"RawAnthropicVertexClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
}
@@ -1,14 +1,28 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_anthropic import (
AnthropicBedrockClient,
AnthropicChatOptions,
AnthropicClient,
AnthropicFoundryClient,
AnthropicVertexClient,
RawAnthropicBedrockClient,
RawAnthropicClient,
RawAnthropicFoundryClient,
RawAnthropicVertexClient,
)
from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions
__all__ = [
"AnthropicBedrockClient",
"AnthropicChatOptions",
"AnthropicClient",
"AnthropicFoundryClient",
"AnthropicVertexClient",
"ClaudeAgent",
"ClaudeAgentOptions",
"RawAnthropicBedrockClient",
"RawAnthropicClient",
"RawAnthropicFoundryClient",
"RawAnthropicVertexClient",
]
@@ -2,13 +2,17 @@
"""Foundry integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from cloud Foundry and Foundry Local connector packages.
This module lazily re-exports objects from:
- ``agent-framework-anthropic``
- ``agent-framework-foundry``
- ``agent-framework-foundry-local``
"""
import importlib
from typing import Any
_IMPORTS: dict[str, tuple[str, str]] = {
"AnthropicFoundryClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"FoundryAgent": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryChatOptions": ("agent_framework_foundry", "agent-framework-foundry"),
@@ -17,6 +21,7 @@ _IMPORTS: dict[str, tuple[str, str]] = {
"FoundryLocalChatOptions": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
"FoundryLocalClient": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
"FoundryLocalSettings": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
"RawAnthropicFoundryClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"RawFoundryAgent": ("agent_framework_foundry", "agent-framework-foundry"),
"RawFoundryAgentChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
"RawFoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
@@ -3,6 +3,7 @@
# Type stubs for the agent_framework.foundry lazy-loading namespace.
# Install the relevant packages for full type support.
from agent_framework_anthropic import AnthropicFoundryClient, RawAnthropicFoundryClient
from agent_framework_foundry import (
FoundryAgent,
FoundryChatClient,
@@ -22,6 +23,7 @@ from agent_framework_foundry_local import (
)
__all__ = [
"AnthropicFoundryClient",
"FoundryAgent",
"FoundryChatClient",
"FoundryChatOptions",
@@ -30,6 +32,7 @@ __all__ = [
"FoundryLocalClient",
"FoundryLocalSettings",
"FoundryMemoryProvider",
"RawAnthropicFoundryClient",
"RawFoundryAgent",
"RawFoundryAgentChatClient",
"RawFoundryChatClient",
@@ -0,0 +1,35 @@
# Copyright (c) Microsoft. All rights reserved.
"""Google integration namespace for optional Agent Framework connectors.
This module lazily re-exports Google-hosted Anthropic clients from:
- ``agent-framework-anthropic``
Supported classes:
- AnthropicVertexClient
- RawAnthropicVertexClient
"""
import importlib
from typing import Any
_IMPORTS: dict[str, tuple[str, str]] = {
"AnthropicVertexClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"RawAnthropicVertexClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
}
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
import_path, package_name = _IMPORTS[name]
try:
return getattr(importlib.import_module(import_path), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{package_name}' package is not installed, please do `pip install {package_name}`"
) from exc
raise AttributeError(f"Module `google` has no attribute {name}.")
def __dir__() -> list[str]:
return list(_IMPORTS.keys())
@@ -0,0 +1,8 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_anthropic import AnthropicVertexClient, RawAnthropicVertexClient
__all__ = [
"AnthropicVertexClient",
"RawAnthropicVertexClient",
]
@@ -1265,15 +1265,13 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
opts: dict[str, Any] = options or {} # type: ignore[assignment]
provider_name = str(getattr(self, "otel_provider_name", "unknown"))
model_id = (
merged_client_kwargs.get("model_id") or opts.get("model_id") or getattr(self, "model_id", None) or "unknown"
)
model = merged_client_kwargs.get("model") or opts.get("model") or getattr(self, "model", None) or "unknown"
service_url_func = getattr(self, "service_url", None)
service_url = str(service_url_func() if callable(service_url_func) else "unknown")
attributes = _get_span_attributes(
operation_name=OtelAttr.CHAT_COMPLETION_OPERATION,
provider_name=provider_name,
model=model_id,
model=model,
service_url=service_url,
**merged_client_kwargs,
)
@@ -1449,13 +1447,13 @@ class EmbeddingTelemetryLayer(Generic[EmbeddingInputT, EmbeddingT, EmbeddingOpti
opts: dict[str, Any] = options or {} # type: ignore[assignment]
provider_name = str(getattr(self, "otel_provider_name", "unknown"))
model_id = opts.get("model_id") or getattr(self, "model_id", None) or "unknown"
model = opts.get("model") or getattr(self, "model", None) or "unknown"
service_url_func = getattr(self, "service_url", None)
service_url = str(service_url_func() if callable(service_url_func) else "unknown")
attributes = _get_span_attributes(
operation_name=OtelAttr.EMBEDDING_OPERATION,
provider_name=provider_name,
model=model_id,
model=model,
service_url=service_url,
)
@@ -1866,8 +1864,7 @@ OTEL_ATTR_MAP: dict[str | tuple[str, ...], tuple[str, Callable[[Any], Any] | Non
"agent_id": (OtelAttr.AGENT_ID, None, False, None),
"agent_name": (OtelAttr.AGENT_NAME, None, False, None),
"agent_description": (OtelAttr.AGENT_DESCRIPTION, None, False, None),
# Multiple source keys - checks model_id in options, then model in kwargs, then model_id in kwargs
("model_id", "model"): (OtelAttr.REQUEST_MODEL, None, True, None),
"model": (OtelAttr.REQUEST_MODEL, None, True, None),
# Tools with validation - returns None if no valid tools
"tools": (
OtelAttr.TOOL_DEFINITIONS,
@@ -2054,8 +2051,8 @@ def _get_response_attributes(
)
if finish_reason:
attributes[OtelAttr.FINISH_REASONS] = json.dumps([finish_reason])
if model_id := getattr(response, "model_id", None):
attributes[OtelAttr.RESPONSE_MODEL] = model_id
if model := getattr(response, "model", None):
attributes[OtelAttr.RESPONSE_MODEL] = model
if capture_usage and (usage := response.usage_details):
input_tokens = usage.get("input_token_count")
if input_tokens:
@@ -154,6 +154,24 @@ def test_chat_client_agent_type(client: SupportsChatGetResponse) -> None:
assert isinstance(chat_client_agent, SupportsAgentRun)
def test_chat_client_agent_uses_client_model_attribute(chat_client_base) -> None:
chat_client_base.model = "claude-model" # type: ignore[attr-defined]
agent = Agent(client=chat_client_base)
assert agent.default_options["model"] == "claude-model"
assert "model_id" not in agent.default_options
def test_chat_client_agent_prefers_default_model_over_client_model(chat_client_base) -> None:
chat_client_base.model = "legacy-model" # type: ignore[attr-defined]
agent = Agent(client=chat_client_base, default_options={"model": "claude-model"})
assert agent.default_options["model"] == "claude-model"
assert "model_id" not in agent.default_options
def test_agent_init_docstring_surfaces_raw_agent_constructor_docs() -> None:
docstring = inspect.getdoc(Agent.__init__)
@@ -1926,6 +1944,20 @@ def test_merge_options_none_values_ignored():
assert result["key2"] == "value2"
def test_merge_options_runtime_model_overrides_default_model() -> None:
"""Test _merge_options lets a runtime model override a default model."""
result = _merge_options({"model": "default-model"}, {"model": "runtime-model"})
assert result["model"] == "runtime-model"
def test_merge_options_preserves_base_model_without_override() -> None:
"""Test _merge_options preserves the base model when there is no override."""
result = _merge_options({"model": "preferred-model"}, {})
assert result["model"] == "preferred-model"
def test_merge_options_tools_combined():
"""Test _merge_options raises when distinct tools share the same name."""
@@ -25,7 +25,7 @@ class MockEmbeddingClient(BaseEmbeddingClient):
options: EmbeddingGenerationOptions | None = None,
) -> GeneratedEmbeddings[list[float]]:
return GeneratedEmbeddings(
[Embedding(vector=[0.1, 0.2, 0.3], model_id="mock-model") for _ in values],
[Embedding(vector=[0.1, 0.2, 0.3], model="mock-model") for _ in values],
usage={"prompt_tokens": len(values), "total_tokens": len(values)},
)
@@ -38,12 +38,12 @@ async def test_base_get_embeddings() -> None:
result = await client.get_embeddings(["hello", "world"])
assert len(result) == 2
assert result[0].vector == [0.1, 0.2, 0.3]
assert result[0].model_id == "mock-model"
assert result[0].model == "mock-model"
async def test_base_get_embeddings_with_options() -> None:
client = MockEmbeddingClient()
options: EmbeddingGenerationOptions = {"model_id": "test", "dimensions": 3}
options: EmbeddingGenerationOptions = {"model": "test", "dimensions": 3}
result = await client.get_embeddings(["hello"], options=options)
assert len(result) == 1
@@ -12,7 +12,7 @@ from agent_framework import Embedding, EmbeddingGenerationOptions, GeneratedEmbe
def test_embedding_basic_construction() -> None:
embedding = Embedding(vector=[0.1, 0.2, 0.3])
assert embedding.vector == [0.1, 0.2, 0.3]
assert embedding.model_id is None
assert embedding.model is None
assert embedding.created_at is None
assert embedding.additional_properties == {}
@@ -21,11 +21,11 @@ def test_embedding_construction_with_metadata() -> None:
now = datetime.now()
embedding = Embedding(
vector=[0.1, 0.2],
model_id="text-embedding-3-small",
model="text-embedding-3-small",
created_at=now,
additional_properties={"key": "value"},
)
assert embedding.model_id == "text-embedding-3-small"
assert embedding.model == "text-embedding-3-small"
assert embedding.created_at == now
assert embedding.additional_properties == {"key": "value"}
@@ -96,7 +96,7 @@ def test_generated_construction_with_usage() -> None:
[
Embedding(
vector=[0.1],
model_id="test-model",
model="test-model",
)
],
usage=usage,
@@ -113,13 +113,13 @@ def test_generated_construction_with_additional_properties() -> None:
def test_generated_construction_with_options() -> None:
opts: EmbeddingGenerationOptions = {"model_id": "text-embedding-3-small", "dimensions": 256}
opts: EmbeddingGenerationOptions = {"model": "text-embedding-3-small", "dimensions": 256}
embeddings = GeneratedEmbeddings(
[Embedding(vector=[0.1])],
options=opts,
)
assert embeddings.options is not None
assert embeddings.options["model_id"] == "text-embedding-3-small"
assert embeddings.options["model"] == "text-embedding-3-small"
assert embeddings.options["dimensions"] == 256
@@ -160,12 +160,12 @@ def test_generated_none_embeddings_creates_empty_list() -> None:
def test_options_empty() -> None:
options: EmbeddingGenerationOptions = {}
assert "model_id" not in options
assert "model" not in options
def test_options_with_model_id() -> None:
options: EmbeddingGenerationOptions = {"model_id": "text-embedding-3-small"}
assert options["model_id"] == "text-embedding-3-small"
def test_options_with_model() -> None:
options: EmbeddingGenerationOptions = {"model": "text-embedding-3-small"}
assert options["model"] == "text-embedding-3-small"
def test_options_with_dimensions() -> None:
@@ -175,8 +175,8 @@ def test_options_with_dimensions() -> None:
def test_options_with_all_fields() -> None:
options: EmbeddingGenerationOptions = {
"model_id": "text-embedding-3-small",
"model": "text-embedding-3-small",
"dimensions": 1536,
}
assert options["model_id"] == "text-embedding-3-small"
assert options["model"] == "text-embedding-3-small"
assert options["dimensions"] == 1536
+10 -10
View File
@@ -1727,7 +1727,7 @@ async def test_mcp_tool_sampling_callback_no_valid_content():
],
)
]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -1778,7 +1778,7 @@ async def test_mcp_tool_sampling_callback_no_response_and_successful_message_cre
tool.client.get_response.return_value = Mock(
messages=[Message(role="assistant", contents=[Content.from_text("Hello")])],
model_id="test-model",
model="test-model",
)
success = await tool.sampling_callback(Mock(), params)
@@ -1808,7 +1808,7 @@ async def test_mcp_tool_sampling_callback_forwards_system_prompt():
mock_chat_client = AsyncMock()
mock_response = Mock()
mock_response.messages = [Message(role="assistant", contents=[Content.from_text("response")])]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -1843,7 +1843,7 @@ async def test_mcp_tool_sampling_callback_forwards_tools():
mock_chat_client = AsyncMock()
mock_response = Mock()
mock_response.messages = [Message(role="assistant", contents=[Content.from_text("response")])]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -1889,7 +1889,7 @@ async def test_mcp_tool_sampling_callback_forwards_tool_choice():
mock_chat_client = AsyncMock()
mock_response = Mock()
mock_response.messages = [Message(role="assistant", contents=[Content.from_text("response")])]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -1924,7 +1924,7 @@ async def test_mcp_tool_sampling_callback_forwards_empty_system_prompt():
mock_chat_client = AsyncMock()
mock_response = Mock()
mock_response.messages = [Message(role="assistant", contents=[Content.from_text("response")])]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -1959,7 +1959,7 @@ async def test_mcp_tool_sampling_callback_forwards_empty_tools_list():
mock_chat_client = AsyncMock()
mock_response = Mock()
mock_response.messages = [Message(role="assistant", contents=[Content.from_text("response")])]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -1994,7 +1994,7 @@ async def test_mcp_tool_sampling_callback_forwards_generation_params_in_options(
mock_chat_client = AsyncMock()
mock_response = Mock()
mock_response.messages = [Message(role="assistant", contents=[Content.from_text("response")])]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -2035,7 +2035,7 @@ async def test_mcp_tool_sampling_callback_omits_temperature_when_none():
mock_chat_client = AsyncMock()
mock_response = Mock()
mock_response.messages = [Message(role="assistant", contents=[Content.from_text("response")])]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -2072,7 +2072,7 @@ async def test_mcp_tool_sampling_callback_always_passes_max_tokens():
mock_chat_client = AsyncMock()
mock_response = Mock()
mock_response.messages = [Message(role="assistant", contents=[Content.from_text("response")])]
mock_response.model_id = "test-model"
mock_response.model = "test-model"
mock_chat_client.get_response.return_value = mock_response
tool.client = mock_chat_client
@@ -207,7 +207,7 @@ async def test_chat_client_observability(mock_chat_client, span_exporter: InMemo
messages = [Message(role="user", text="Test message")]
span_exporter.clear()
response = await client.get_response(messages=messages, options={"model_id": "Test"})
response = await client.get_response(messages=messages, options={"model": "Test"})
assert response is not None
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
@@ -222,6 +222,23 @@ async def test_chat_client_observability(mock_chat_client, span_exporter: InMemo
assert span.attributes[OtelAttr.OUTPUT_MESSAGES] is not None
@pytest.mark.parametrize("enable_sensitive_data", [True, False], indirect=True)
async def test_chat_client_observability_accepts_model_option(
mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data
):
"""Test that telemetry also captures the modern model option."""
client = mock_chat_client()
messages = [Message(role="user", text="Test message")]
span_exporter.clear()
response = await client.get_response(messages=messages, options={"model": "Test"})
assert response is not None
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
span = spans[0]
assert span.attributes[OtelAttr.REQUEST_MODEL] == "Test"
@pytest.mark.parametrize("enable_sensitive_data", [True, False], indirect=True)
async def test_chat_client_streaming_observability(
mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data
@@ -232,7 +249,7 @@ async def test_chat_client_streaming_observability(
span_exporter.clear()
# Collect all yielded updates
updates = []
stream = client.get_response(stream=True, messages=messages, options={"model_id": "Test"})
stream = client.get_response(stream=True, messages=messages, options={"model": "Test"})
async for update in stream:
updates.append(update)
await stream.get_final_response()
@@ -260,7 +277,7 @@ async def test_chat_client_observability_with_instructions(
client = mock_chat_client()
messages = [Message(role="user", text="Test message")]
options = {"model_id": "Test", "instructions": "You are a helpful assistant."}
options = {"model": "Test", "instructions": "You are a helpful assistant."}
span_exporter.clear()
response = await client.get_response(messages=messages, options=options)
@@ -289,7 +306,7 @@ async def test_chat_client_streaming_observability_with_instructions(
client = mock_chat_client()
messages = [Message(role="user", text="Test")]
options = {"model_id": "Test", "instructions": "You are a helpful assistant."}
options = {"model": "Test", "instructions": "You are a helpful assistant."}
span_exporter.clear()
updates = []
@@ -318,7 +335,7 @@ async def test_chat_client_observability_without_instructions(
client = mock_chat_client()
messages = [Message(role="user", text="Test message")]
options = {"model_id": "Test"} # No instructions
options = {"model": "Test"} # No instructions
span_exporter.clear()
response = await client.get_response(messages=messages, options=options)
@@ -339,7 +356,7 @@ async def test_chat_client_observability_with_empty_instructions(
client = mock_chat_client()
messages = [Message(role="user", text="Test message")]
options = {"model_id": "Test", "instructions": ""} # Empty string
options = {"model": "Test", "instructions": ""} # Empty string
span_exporter.clear()
response = await client.get_response(messages=messages, options=options)
@@ -362,7 +379,7 @@ async def test_chat_client_observability_with_list_instructions(
client = mock_chat_client()
messages = [Message(role="user", text="Test message")]
options = {"model_id": "Test", "instructions": ["Instruction 1", "Instruction 2"]}
options = {"model": "Test", "instructions": ["Instruction 1", "Instruction 2"]}
span_exporter.clear()
response = await client.get_response(messages=messages, options=options)
@@ -379,8 +396,8 @@ async def test_chat_client_observability_with_list_instructions(
assert system_instructions[1]["content"] == "Instruction 2"
async def test_chat_client_without_model_id_observability(mock_chat_client, span_exporter: InMemorySpanExporter):
"""Test telemetry shouldn't fail when the model_id is not provided for unknown reason."""
async def test_chat_client_without_model_observability(mock_chat_client, span_exporter: InMemorySpanExporter):
"""Test telemetry shouldn't fail when the model is not provided for unknown reason."""
client = mock_chat_client()
messages = [Message(role="user", text="Test")]
span_exporter.clear()
@@ -396,10 +413,8 @@ async def test_chat_client_without_model_id_observability(mock_chat_client, span
assert span.attributes[OtelAttr.REQUEST_MODEL] == "unknown"
async def test_chat_client_streaming_without_model_id_observability(
mock_chat_client, span_exporter: InMemorySpanExporter
):
"""Test streaming telemetry shouldn't fail when the model_id is not provided for unknown reason."""
async def test_chat_client_streaming_without_model_observability(mock_chat_client, span_exporter: InMemorySpanExporter):
"""Test streaming telemetry shouldn't fail when the model is not provided for unknown reason."""
client = mock_chat_client()
messages = [Message(role="user", text="Test")]
span_exporter.clear()
@@ -441,7 +456,7 @@ def mock_chat_agent():
self.id = "test_agent_id"
self.name = "test_agent"
self.description = "Test agent description"
self.default_options: dict[str, Any] = {"model_id": "TestModel"}
self.default_options: dict[str, Any] = {"model": "TestModel"}
def run(self, messages=None, *, session=None, stream=False, **kwargs):
if stream:
@@ -1540,7 +1555,7 @@ async def test_chat_client_observability_exception(mock_chat_client, span_export
span_exporter.clear()
with pytest.raises(ValueError, match="Test error"):
await client.get_response(messages=messages, options={"model_id": "Test"})
await client.get_response(messages=messages, options={"model": "Test"})
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
@@ -1570,7 +1585,7 @@ async def test_chat_client_streaming_observability_exception(mock_chat_client, s
span_exporter.clear()
with pytest.raises(ValueError, match="Streaming error"):
async for _ in client.get_response(messages=messages, stream=True, options={"model_id": "Test"}):
async for _ in client.get_response(messages=messages, stream=True, options={"model": "Test"}):
pass
spans = span_exporter.get_finished_spans()
@@ -1651,8 +1666,8 @@ def test_get_response_attributes_with_finish_reason():
assert OtelAttr.FINISH_REASONS in result
def test_get_response_attributes_with_model_id():
"""Test _get_response_attributes includes model_id."""
def test_get_response_attributes_with_model():
"""Test _get_response_attributes includes model."""
from unittest.mock import Mock
from agent_framework.observability import _get_response_attributes
@@ -1662,7 +1677,7 @@ def test_get_response_attributes_with_model_id():
response.finish_reason = None
response.raw_representation = None
response.usage_details = None
response.model_id = "gpt-4"
response.model = "gpt-4"
attrs = {}
result = _get_response_attributes(attrs, response)
@@ -2075,7 +2090,7 @@ async def test_capture_messages_with_finish_reason(mock_chat_client, span_export
messages = [Message(role="user", text="Test")]
span_exporter.clear()
response = await client.get_response(messages=messages, options={"model_id": "Test"})
response = await client.get_response(messages=messages, options={"model": "Test"})
assert response is not None
assert response.finish_reason == "stop"
@@ -2165,7 +2180,7 @@ async def test_chat_client_when_disabled(mock_chat_client, span_exporter: InMemo
messages = [Message(role="user", text="Test")]
span_exporter.clear()
response = await client.get_response(messages=messages, options={"model_id": "Test"})
response = await client.get_response(messages=messages, options={"model": "Test"})
assert response is not None
spans = span_exporter.get_finished_spans()
@@ -2181,7 +2196,7 @@ async def test_chat_client_streaming_when_disabled(mock_chat_client, span_export
span_exporter.clear()
updates = []
async for update in client.get_response(messages=messages, stream=True, options={"model_id": "Test"}):
async for update in client.get_response(messages=messages, stream=True, options={"model": "Test"}):
updates.append(update)
assert len(updates) == 2 # Still works functionally
@@ -2501,7 +2516,7 @@ async def test_layer_ordering_span_sequence_with_function_calling(span_exporter:
def __init__(self):
super().__init__()
self.call_count = 0
self.model_id = "test-model"
self.model = "test-model"
def service_url(self):
return "https://test.example.com"
@@ -2608,7 +2623,7 @@ async def test_agent_and_chat_spans_do_not_duplicate_response_telemetry(
id="nested_agent_id",
name="nested_agent",
description="Nested telemetry agent",
default_options={"model_id": "NestedModel"},
default_options={"model": "NestedModel"},
)
span_exporter.clear()
@@ -2661,7 +2676,7 @@ async def test_capture_messages_preserves_non_ascii_characters(mock_chat_client,
messages = [Message(role="user", text=japanese_text)]
span_exporter.clear()
response = await client.get_response(messages=messages, options={"model_id": "Test"})
response = await client.get_response(messages=messages, options={"model": "Test"})
assert response is not None
spans = span_exporter.get_finished_spans()
@@ -2825,7 +2840,7 @@ async def test_agent_instructions_from_default_options(
import json
agent = mock_chat_agent()
agent.default_options = {"model_id": "TestModel", "instructions": "Default system instructions."}
agent.default_options = {"model": "TestModel", "instructions": "Default system instructions."}
messages = [Message(role="user", text="Test message")]
span_exporter.clear()
@@ -2851,7 +2866,7 @@ async def test_agent_instructions_from_options_override(
import json
agent = mock_chat_agent()
agent.default_options = {"model_id": "TestModel"} # No default instructions
agent.default_options = {"model": "TestModel"} # No default instructions
messages = [Message(role="user", text="Test message")]
span_exporter.clear()
@@ -2876,7 +2891,7 @@ async def test_agent_instructions_merged_from_default_and_options(
import json
agent = mock_chat_agent()
agent.default_options = {"model_id": "TestModel", "instructions": "Default instructions."}
agent.default_options = {"model": "TestModel", "instructions": "Default instructions."}
messages = [Message(role="user", text="Test message")]
span_exporter.clear()
@@ -2903,7 +2918,7 @@ async def test_agent_streaming_instructions_from_default_options(
import json
agent = mock_chat_agent()
agent.default_options = {"model_id": "TestModel", "instructions": "Default streaming instructions."}
agent.default_options = {"model": "TestModel", "instructions": "Default streaming instructions."}
messages = [Message(role="user", text="Test message")]
span_exporter.clear()
@@ -2932,7 +2947,7 @@ async def test_agent_streaming_instructions_merged_from_default_and_options(
import json
agent = mock_chat_agent()
agent.default_options = {"model_id": "TestModel", "instructions": "Default instructions."}
agent.default_options = {"model": "TestModel", "instructions": "Default instructions."}
messages = [Message(role="user", text="Test message")]
span_exporter.clear()
@@ -2960,7 +2975,7 @@ async def test_agent_no_instructions_in_default_or_options(
):
"""Test that system_instructions is not set when neither default_options nor options have instructions."""
agent = mock_chat_agent()
agent.default_options = {"model_id": "TestModel"} # No instructions
agent.default_options = {"model": "TestModel"} # No instructions
messages = [Message(role="user", text="Test message")]
span_exporter.clear()
@@ -608,7 +608,7 @@ async def test_tool_invoke_rejects_unexpected_runtime_kwargs() -> None:
await simple_tool.invoke(
arguments=args,
api_token="secret-token",
options={"model_id": "dummy"},
options={"model": "dummy"},
)
+20 -4
View File
@@ -754,6 +754,14 @@ def test_chat_response():
assert str(response) == response.text
def test_chat_response_accepts_model_alias() -> None:
"""Test ChatResponse accepts model and exposes it through model alias."""
response = ChatResponse(messages=Message(role="assistant", text="Hello"), model="claude-test")
assert response.model == "claude-test"
assert response.model == "claude-test"
class OutputModel(BaseModel):
response: str
@@ -851,6 +859,14 @@ def test_chat_response_update():
assert response_update.text == "I'm doing well, thank you!"
def test_chat_response_update_accepts_model_alias() -> None:
"""Test ChatResponseUpdate accepts model and exposes it through model alias."""
response_update = ChatResponseUpdate(contents=[Content.from_text("Hello")], model="claude-test")
assert response_update.model == "claude-test"
assert response_update.model == "claude-test"
def test_chat_response_updates_to_chat_response_one():
"""Test converting ChatResponseUpdate to ChatResponse."""
# Create a Message
@@ -1374,7 +1390,7 @@ def test_response_update_propagates_fields_and_metadata():
response_id="rid",
message_id="mid",
conversation_id="cid",
model_id="model-x",
model="model-x",
created_at="t0",
finish_reason="stop",
additional_properties={"k": "v"},
@@ -1383,7 +1399,7 @@ def test_response_update_propagates_fields_and_metadata():
assert resp.response_id == "rid"
assert resp.created_at == "t0"
assert resp.conversation_id == "cid"
assert resp.model_id == "model-x"
assert resp.model == "model-x"
assert resp.finish_reason == "stop"
assert resp.additional_properties and resp.additional_properties["k"] == "v"
assert resp.messages[0].role == "assistant"
@@ -1935,7 +1951,7 @@ def test_chat_response_complex_serialization():
assert isinstance(response.messages[0], Message)
assert isinstance(response.finish_reason, str) # FinishReason is now a NewType of str
assert isinstance(response.usage_details, dict)
assert response.model_id == "gpt-4" # Should be stored as model_id
assert response.model == "gpt-4" # Should be stored as model
# Test to_dict with complex objects
response_dict = response.to_dict()
@@ -1943,7 +1959,7 @@ def test_chat_response_complex_serialization():
assert isinstance(response_dict["messages"][0], dict)
assert isinstance(response_dict["finish_reason"], str) # FinishReason serializes to string
assert isinstance(response_dict["usage_details"], dict)
assert response_dict["model"] == "gpt-4" # Should serialize as model_id
assert response_dict["model"] == "gpt-4" # Should serialize as model
def test_chat_response_update_all_content_types():
@@ -46,7 +46,7 @@ else:
class ProviderTypeMapping(TypedDict, total=True):
package: str
name: str
model_id_field: str
model_field: str
endpoint_field: str | None
api_key_field: str | None
@@ -55,63 +55,63 @@ PROVIDER_TYPE_OBJECT_MAPPING: dict[str, ProviderTypeMapping] = {
"AzureOpenAI": {
"package": "agent_framework.openai",
"name": "OpenAIChatClient",
"model_id_field": "model",
"model_field": "model",
"endpoint_field": "azure_endpoint",
"api_key_field": "api_key",
},
"AzureOpenAI.Chat": {
"package": "agent_framework.openai",
"name": "OpenAIChatCompletionClient",
"model_id_field": "model",
"model_field": "model",
"endpoint_field": "azure_endpoint",
"api_key_field": "api_key",
},
"AzureOpenAI.Responses": {
"package": "agent_framework.openai",
"name": "OpenAIChatClient",
"model_id_field": "model",
"model_field": "model",
"endpoint_field": "azure_endpoint",
"api_key_field": "api_key",
},
"Foundry": {
"package": "agent_framework.foundry",
"name": "FoundryChatClient",
"model_id_field": "model",
"model_field": "model",
"endpoint_field": "project_endpoint",
"api_key_field": None,
},
"OpenAI.Chat": {
"package": "agent_framework.openai",
"name": "OpenAIChatClient",
"model_id_field": "model",
"model_field": "model",
"endpoint_field": "base_url",
"api_key_field": "api_key",
},
"OpenAI.Responses": {
"package": "agent_framework.openai",
"name": "OpenAIChatClient",
"model_id_field": "model",
"model_field": "model",
"endpoint_field": "base_url",
"api_key_field": "api_key",
},
"OpenAI": {
"package": "agent_framework.openai",
"name": "OpenAIChatClient",
"model_id_field": "model",
"model_field": "model",
"endpoint_field": "base_url",
"api_key_field": "api_key",
},
"Foundry.Chat": {
"package": "agent_framework.foundry",
"name": "FoundryChatClient",
"model_id_field": "model",
"model_field": "model",
"endpoint_field": "project_endpoint",
"api_key_field": None,
},
"Anthropic.Chat": {
"package": "agent_framework.anthropic",
"name": "AnthropicChatClient",
"model_id_field": "model_id",
"model_field": "model",
"endpoint_field": None,
"api_key_field": "api_key",
},
@@ -210,7 +210,7 @@ class AgentFactory:
"Provider.ApiType": {
"package": "package.name",
"name": "ClassName",
"model_id_field": "field_name_in_constructor",
"model_field": "field_name_in_constructor",
"endpoint_field": "endpoint_kwarg_name_or_null",
"api_key_field": "api_key_kwarg_name_or_null",
},
@@ -220,7 +220,7 @@ class AgentFactory:
Here, "Provider.ApiType" is the lookup key used when both provider and apiType are specified in the
model, "Provider" is also allowed.
Package refers to which model needs to be imported, Name is the class name of the
SupportsChatGetResponse implementation, and model_id_field is the name of the field in the
SupportsChatGetResponse implementation, and model_field is the name of the field in the
constructor that accepts the model.id value.
default_provider: The default provider used when model.provider is not specified,
default is "OpenAI".
@@ -264,7 +264,7 @@ class AgentFactory:
"CustomProvider.Chat": {
"package": "my_package.clients",
"name": "CustomChatClient",
"model_id_field": "model_name",
"model_field": "model",
},
},
)
@@ -690,7 +690,7 @@ class AgentFactory:
# if prompt_agent.model is defined, but no id, use the supplied client
if self.client:
return self.client
# or raise, since we cannot create a client without model id
# or raise, since we cannot create a client without a model
raise DeclarativeLoaderError(
"ChatClient must be provided to create agent from PromptAgent, or define model.id in the PromptAgent."
)
@@ -699,7 +699,7 @@ class AgentFactory:
class_name = mapping["name"]
module = __import__(module_name, fromlist=[class_name])
agent_class = getattr(module, class_name)
setup_dict[mapping["model_id_field"]] = prompt_agent.model.id
setup_dict[mapping["model_field"]] = prompt_agent.model.id
return agent_class(**setup_dict) # type: ignore[no-any-return]
def _parse_chat_options(self, model: Model | None) -> dict[str, Any]:
@@ -841,7 +841,7 @@ class AgentFactory:
model: The Model instance containing provider and apiType information.
Returns:
A dictionary containing the package, name, and model_id_field for the provider.
A dictionary containing the package, name, and model_field for the provider.
Raises:
ProviderLookupError: If the provider type is not supported or can't be found.
@@ -632,7 +632,7 @@ class TestAgentFactorySafeMode:
from agent_framework_declarative._loader import AgentFactory
monkeypatch.setenv("TEST_MODEL_ID", "gpt-4-from-env")
monkeypatch.setenv("TEST_MODEL", "gpt-4-from-env")
# Create a mock chat client to avoid needing real provider
mock_client = MagicMock()
@@ -1131,7 +1131,7 @@ model:
"CustomProvider.Chat": {
"package": "agent_framework.openai",
"name": "OpenAIChatClient",
"model_id_field": "model_id",
"model_field": "model",
},
}
@@ -391,7 +391,7 @@ class EntityDiscovery:
source=source, # IMPORTANT: Pass the source parameter
tools=[str(tool) for tool in (tools_list or [])],
instructions=instructions,
model_id=model,
model=model,
chat_client_type=chat_client_type,
context_provider=context_provider_list,
middleware=middlewares_list,
@@ -846,7 +846,7 @@ class EntityDiscovery:
description=description,
tools=tools_union,
instructions=instructions,
model_id=model,
model=model,
chat_client_type=chat_client_type,
context_provider=context_provider_list,
middleware=middlewares_list,
@@ -770,9 +770,9 @@ class MessageMapper:
from .models._openai_custom import AgentCompletedEvent, AgentFailedEvent, AgentStartedEvent
try:
# Get model name from request or use 'devui' as default
# Get model from request or use 'devui' as default
request_obj = context.get("request")
model_name = request_obj.model if request_obj and request_obj.model else "devui"
model = request_obj.model if request_obj and request_obj.model else "devui"
if isinstance(event, AgentStartedEvent):
execution_id = f"agent_{uuid4().hex[:12]}"
@@ -783,7 +783,7 @@ class MessageMapper:
id=f"resp_{execution_id}",
object="response",
created_at=float(time.time()),
model=model_name,
model=model,
output=[],
status="in_progress",
parallel_tool_calls=False,
@@ -818,7 +818,7 @@ class MessageMapper:
id=f"resp_{execution_id}",
object="response",
created_at=float(time.time()),
model=model_name,
model=model,
output=[],
status="failed",
error=response_error,
@@ -865,16 +865,16 @@ class MessageMapper:
# Return proper OpenAI event objects
events: list[Any] = []
# Get model name from request or use 'devui' as default
# Get model from request or use 'devui' as default
request_obj = context.get("request")
model_name = request_obj.model if request_obj and request_obj.model else "devui"
model = request_obj.model if request_obj and request_obj.model else "devui"
# Create a full Response object with all required fields
response_obj = Response(
id=f"resp_{workflow_id}",
object="response",
created_at=float(time.time()),
model=model_name,
model=model,
output=[], # Empty output list initially
status="in_progress",
# Required fields with safe defaults
@@ -989,9 +989,9 @@ class MessageMapper:
# Import Response and ResponseError types
from openai.types.responses import Response, ResponseError
# Get model name from request or use 'devui' as default
# Get model from request or use 'devui' as default
request_obj = context.get("request")
model_name = request_obj.model if request_obj and request_obj.model else "devui"
model = request_obj.model if request_obj and request_obj.model else "devui"
# Extract error message from WorkflowErrorDetails
if details:
@@ -1013,7 +1013,7 @@ class MessageMapper:
id=f"resp_{workflow_id}",
object="response",
created_at=float(time.time()),
model=model_name,
model=model,
output=[],
status="failed",
error=response_error,
@@ -20,7 +20,7 @@ class RequestRecord(TypedDict):
entity_id: str
executor: str
input: Any
model_id: str
model: str
stream: bool
execution_time: NotRequired[float]
status: NotRequired[str]
@@ -91,7 +91,7 @@ class SessionManager:
logger.debug(f"Closed session: {session_id}")
def add_request_record(
self, session_id: str, entity_id: str, executor_name: str, request_input: Any, model_id: str
self, session_id: str, entity_id: str, executor_name: str, request_input: Any, model: str
) -> str:
"""Add a request record to a session.
@@ -100,7 +100,7 @@ class SessionManager:
entity_id: ID of the entity being executed
executor_name: Name of the executor
request_input: Input for the request
model_id: Model name
model: Model name
Returns:
Request ID
@@ -115,7 +115,7 @@ class SessionManager:
"entity_id": entity_id,
"executor": executor_name,
"input": request_input,
"model_id": model_id,
"model": model,
"stream": True,
}
session["requests"].append(request_record)
@@ -163,7 +163,7 @@ class SessionManager:
"timestamp": req["timestamp"].isoformat(),
"entity_id": req["entity_id"],
"executor": req["executor"],
"model": req["model_id"],
"model": req["model"],
"input_length": len(str(req["input"])) if req["input"] else 0,
"execution_time": req.get("execution_time"),
"status": req.get("status", "unknown"),
@@ -59,13 +59,13 @@ def extract_agent_metadata(entity_object: Any) -> dict[str, Any]:
chat_opts = entity_object.default_options
chat_opts_dict = _string_key_dict(chat_opts)
if chat_opts_dict is not None:
model_id = chat_opts_dict.get("model_id")
if model_id:
metadata["model"] = model_id
elif hasattr(chat_opts, "model_id") and chat_opts.model_id:
metadata["model"] = chat_opts.model_id
if metadata["model"] is None and hasattr(entity_object, "client") and hasattr(entity_object.client, "model_id"):
metadata["model"] = entity_object.client.model_id
model = chat_opts_dict.get("model")
if model:
metadata["model"] = model
elif hasattr(chat_opts, "model") and chat_opts.model:
metadata["model"] = chat_opts.model
if metadata["model"] is None and hasattr(entity_object, "client") and hasattr(entity_object.client, "model"):
metadata["model"] = entity_object.client.model
# Try to get chat client type
if hasattr(entity_object, "client"):
@@ -44,7 +44,7 @@ class EntityInfo(BaseModel):
# Agent-specific fields (optional, populated when available)
instructions: str | None = None
model_id: str | None = None
model: str | None = None
chat_client_type: str | None = None
context_provider: list[str] | None = None
middleware: list[str] | None = None
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -37,7 +37,7 @@ OPENAI_CHAT_MODEL="gpt-4o-mini"
# Or for Azure OpenAI
AZURE_OPENAI_ENDPOINT="your-endpoint"
AZURE_OPENAI_DEPLOYMENT_NAME="your-deployment-name"
AZURE_OPENAI_MODEL="your-deployment-name"
```
## 4. Test DevUI
@@ -247,7 +247,7 @@ services:
# Or Azure OpenAI
- AZURE_OPENAI_API_KEY=\${AZURE_OPENAI_API_KEY}
- AZURE_OPENAI_ENDPOINT=\${AZURE_OPENAI_ENDPOINT}
- AZURE_OPENAI_DEPLOYMENT_NAME=\${AZURE_OPENAI_DEPLOYMENT_NAME}
- AZURE_OPENAI_MODEL=\${AZURE_OPENAI_MODEL}
# Optional: Enable instrumentation
- ENABLE_INSTRUMENTATION=\${ENABLE_INSTRUMENTATION:-false}
ports:
@@ -78,7 +78,7 @@ export const SAMPLE_ENTITIES: SampleEntity[] = [
required: true,
},
{
name: "AZURE_OPENAI_DEPLOYMENT_NAME",
name: "AZURE_OPENAI_MODEL",
description: "Name of the deployed model in Azure OpenAI",
required: true,
example: "gpt-4o",
@@ -151,7 +151,7 @@ async def test_credential_cleanup() -> None:
# Create mock chat client with credential
mock_client = Mock()
mock_client.async_credential = mock_credential
mock_client.model_id = "test-model"
mock_client.model = "test-model"
mock_client.function_invocation_configuration = None
# Create agent with mock client
@@ -184,7 +184,7 @@ async def test_credential_cleanup_error_handling() -> None:
# Create mock chat client with credential
mock_client = Mock()
mock_client.async_credential = mock_credential
mock_client.model_id = "test-model"
mock_client.model = "test-model"
mock_client.function_invocation_configuration = None
# Create agent with mock client
@@ -219,7 +219,7 @@ async def test_multiple_credential_attributes() -> None:
mock_client = Mock()
mock_client.credential = mock_cred1
mock_client.async_credential = mock_cred2
mock_client.model_id = "test-model"
mock_client.model = "test-model"
mock_client.function_invocation_configuration = None
# Create agent with mock client
@@ -1,6 +1,6 @@
# Azure OpenAI Configuration
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT_NAME=your-deployment-name
AZURE_OPENAI_MODEL=your-deployment-name
# Optional: Use Azure CLI authentication if not provided
# AZURE_OPENAI_API_KEY=your-api-key
@@ -14,7 +14,7 @@ cp .env.example .env
Required variables:
- `AZURE_OPENAI_ENDPOINT`
- `AZURE_OPENAI_DEPLOYMENT_NAME`
- `AZURE_OPENAI_MODEL`
- `AZURE_OPENAI_API_KEY` (optional if using Azure CLI authentication)
- `ENDPOINT` (default: http://localhost:8080)
- `TASKHUB` (default: default)
@@ -97,7 +97,7 @@ If you see "DTS emulator is not available":
If you see authentication or deployment errors:
- Verify your `AZURE_OPENAI_ENDPOINT` is correct
- Confirm `AZURE_OPENAI_DEPLOYMENT_NAME` matches your deployment
- Confirm `AZURE_OPENAI_MODEL` matches your deployment
- If using API key, check `AZURE_OPENAI_API_KEY` is valid
- If using Azure CLI, ensure you're logged in: `az login`
@@ -291,7 +291,7 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item
"""Skip tests based on markers and environment availability."""
foundry_vars = ["FOUNDRY_PROJECT_ENDPOINT", "FOUNDRY_MODEL"]
foundry_available = all(os.getenv(var) for var in foundry_vars)
azure_openai_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT_NAME"]
azure_openai_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_MODEL"]
azure_openai_available = all(os.getenv(var) for var in azure_openai_vars)
skip_foundry = pytest.mark.skip(reason=f"Missing required environment variables: {', '.join(foundry_vars)}")
skip_azure_openai = pytest.mark.skip(
@@ -348,7 +348,7 @@ def check_sample_env(request: pytest.FixtureRequest) -> None:
sample_name = cast(str, sample_marker.args[0]) # type: ignore[union-attr]
if sample_name == "06_multi_agent_orchestration_conditionals":
required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT_NAME"]
required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_MODEL"]
else:
required_vars = ["FOUNDRY_PROJECT_ENDPOINT", "FOUNDRY_MODEL"]
missing = [var for var in required_vars if not os.getenv(var)]
@@ -477,15 +477,14 @@ class FoundryChatClient( # type: ignore[misc]
- ``FOUNDRY_MODEL`` to provide the Foundry model deployment name.
Keyword Args:
project_endpoint: The Foundry project endpoint URL.
Can also be set via environment variable ``FOUNDRY_PROJECT_ENDPOINT``.
project_client: An existing AIProjectClient to use.
model: The model deployment name.
Can also be set via environment variable ``FOUNDRY_MODEL``.
model_id: Deprecated alias for ``model``.
credential: Azure credential or token provider for authentication.
allow_preview: Enables preview opt-in on internally-created AIProjectClient.
env_file_path: Path to .env file for settings.
project_endpoint: The Foundry project endpoint URL.
Can also be set via environment variable ``FOUNDRY_PROJECT_ENDPOINT``.
project_client: An existing AIProjectClient to use.
model: The model deployment name.
Can also be set via environment variable ``FOUNDRY_MODEL``.
credential: Azure credential or token provider for authentication.
allow_preview: Enables preview opt-in on internally-created AIProjectClient.
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
instruction_role: The role to use for 'instruction' messages.
middleware: Optional sequence of middleware.
+1 -1
View File
@@ -13,7 +13,7 @@ Integration with Azure AI Foundry Local for local model inference.
```python
from agent_framework.foundry import FoundryLocalClient
client = FoundryLocalClient(model_id="your-local-model")
client = FoundryLocalClient(model="your-local-model")
response = await client.get_response("Hello")
```
@@ -23,7 +23,7 @@ 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
from foundry_local.models import DeviceType, FoundryModelInfo
from openai import AsyncOpenAI
from pydantic import BaseModel
@@ -60,7 +60,7 @@ class FoundryLocalChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModel
Keys:
# Inherited from ChatOptions (supported via OpenAI-compatible API):
model_id: The model identifier or alias (e.g., 'phi-4-mini').
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.
@@ -104,11 +104,6 @@ class FoundryLocalChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModel
"""Not applicable for local inference."""
FOUNDRY_LOCAL_OPTION_TRANSLATIONS: dict[str, str] = {
"model_id": "model",
}
"""Maps ChatOptions keys to OpenAI API parameter names (for compatibility)."""
FoundryLocalChatOptionsT = TypeVar(
"FoundryLocalChatOptionsT",
bound=TypedDict, # type: ignore[valid-type]
@@ -295,8 +290,8 @@ class FoundryLocalClient(
# 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(alias_or_model_id="phi-4-mini", device=DeviceType.CPU)
client.manager.load_model(alias_or_model_id="phi-4-mini", device=DeviceType.CPU)
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`
@@ -328,8 +323,8 @@ class FoundryLocalClient(
model_setting: str = settings["model"] # type: ignore[assignment] # pyright: ignore[reportTypedDictNotRequiredAccess]
manager = FoundryLocalManager(bootstrap=bootstrap, timeout=timeout)
model_info = manager.get_model_info(
alias_or_model_id=model_setting,
model_info: FoundryModelInfo | None = manager.get_model_info(
model_setting,
device=device,
)
if model_info is None:
@@ -340,8 +335,8 @@ class FoundryLocalClient(
)
raise ValueError(message)
if prepare_model:
manager.download_model(alias_or_model_id=model_info.id, device=device)
manager.load_model(alias_or_model_id=model_info.id, device=device)
manager.download_model(model_info.id, device=device)
manager.load_model(model_info.id, device=device)
super().__init__(
model=model_info.id,
@@ -34,7 +34,7 @@ def test_foundry_local_settings_init_with_explicit_values() -> None:
@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_id is missing raises error."""
"""Test FoundryLocalSettings when model is missing raises error."""
with pytest.raises(SettingNotFoundError, match="Required setting 'model'"):
load_settings(
FoundryLocalSettings,
@@ -157,15 +157,15 @@ def test_foundry_local_client_init_with_device(mock_foundry_local_manager: Magic
FoundryLocalClient(model="test-model-id", device=DeviceType.CPU)
mock_foundry_local_manager.get_model_info.assert_called_once_with(
alias_or_model_id="test-model-id",
"test-model-id",
device=DeviceType.CPU,
)
mock_foundry_local_manager.download_model.assert_called_once_with(
alias_or_model_id="test-model-id",
"test-model-id",
device=DeviceType.CPU,
)
mock_foundry_local_manager.load_model.assert_called_once_with(
alias_or_model_id="test-model-id",
"test-model-id",
device=DeviceType.CPU,
)
@@ -207,10 +207,10 @@ def test_foundry_local_client_init_calls_download_and_load(mock_foundry_local_ma
FoundryLocalClient(model="test-model-id")
mock_foundry_local_manager.download_model.assert_called_once_with(
alias_or_model_id="test-model-id",
"test-model-id",
device=None,
)
mock_foundry_local_manager.load_model.assert_called_once_with(
alias_or_model_id="test-model-id",
"test-model-id",
device=None,
)
+1 -1
View File
@@ -51,7 +51,7 @@ async def math_agent(task: TaskType, llm: LLM) -> float:
MCPStdioTool(name="calculator", command="uvx", args=["mcp-server-calculator"]) as mcp_server,
Agent(
client=OpenAIChatClient(
model_id=llm.model,
model=llm.model,
api_key="your-api-key",
base_url=llm.endpoint,
),
@@ -169,7 +169,7 @@ async def math_agent(task: MathProblem, llm: LLM) -> float:
MCPStdioTool(name="calculator", command="uvx", args=["mcp-server-calculator"]) as mcp_server,
Agent(
client=OpenAIChatClient(
model_id=llm.model, # This is the model being trained
model=llm.model, # This is the model being trained
api_key=os.getenv("OPENAI_API_KEY") or "dummy", # Can be dummy when connecting to training LLM
base_url=llm.endpoint, # vLLM server endpoint provided by agent-lightning
),
@@ -106,14 +106,14 @@ class Tau2Agent(LitAgent):
assistant_chat_client = OpenAIChatClient(
base_url=llm.endpoint, # vLLM endpoint for the model being trained
api_key=openai_api_key,
model_id=llm.model, # Model ID being trained
model=llm.model, # Model ID being trained
)
# User simulator: uses a fixed, capable model for consistent simulation
user_simulator_chat_client = OpenAIChatClient(
base_url=openai_base_url, # External API endpoint
api_key=openai_api_key,
model_id="gpt-4.1", # Fixed model for user simulator
model="gpt-4.1", # Fixed model for user simulator
)
try:
+2 -2
View File
@@ -67,12 +67,12 @@ async def run_single_task():
assistant_client = OpenAIChatClient(
base_url="https://api.openai.com/v1",
api_key="your-api-key",
model_id="gpt-4o"
model="gpt-4o"
)
user_client = OpenAIChatClient(
base_url="https://api.openai.com/v1",
api_key="your-api-key",
model_id="gpt-4o-mini"
model="gpt-4o-mini"
)
# Get a task and run it
@@ -96,14 +96,14 @@ async def run_benchmark(assistant_model: str, user_model: str, debug_task_id: st
assistant_chat_client = OpenAIChatClient(
base_url=openai_base_url,
api_key=openai_api_key,
model_id=assistant_model,
model=assistant_model,
)
# User simulator: simulates realistic customer behavior and requests
user_simulator_chat_client = OpenAIChatClient(
base_url=openai_base_url,
api_key=openai_api_key,
model_id=user_model,
model=user_model,
)
# STEP 4: Filter task set for debug mode
@@ -133,8 +133,8 @@ async def run_benchmark(assistant_model: str, user_model: str, debug_task_id: st
# Initialize result structure for this task
result: dict[str, Any] = {
"config": {
"assistant": assistant_chat_client.model_id,
"user": user_simulator_chat_client.model_id,
"assistant": assistant_chat_client.model,
"user": user_simulator_chat_client.model,
},
"task": task,
}
@@ -183,8 +183,8 @@ async def run_benchmark(assistant_model: str, user_model: str, debug_task_id: st
# Initialize result structure for this task
result: dict[str, Any] = {
"config": {
"assistant": assistant_chat_client.model_id,
"user": user_simulator_chat_client.model_id,
"assistant": assistant_chat_client.model,
"user": user_simulator_chat_client.model,
},
"task": task,
}
+1 -1
View File
@@ -13,7 +13,7 @@ Integration with Ollama for local LLM inference.
```python
from agent_framework.ollama import OllamaChatClient
client = OllamaChatClient(model_id="llama3.2")
client = OllamaChatClient(model="llama3.2")
response = await client.get_response("Hello")
```
@@ -77,7 +77,7 @@ class OllamaChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to
Keys:
# Inherited from ChatOptions (mapped to Ollama options):
model_id: The model name, translates to ``model`` in Ollama API.
model: The model name, translates to ``model`` in Ollama API.
temperature: Sampling temperature, translates to ``options.temperature``.
top_p: Nucleus sampling, translates to ``options.top_p``.
max_tokens: Maximum tokens to generate, translates to ``options.num_predict``.
@@ -229,7 +229,6 @@ class OllamaChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to
OLLAMA_OPTION_TRANSLATIONS: dict[str, str] = {
"model_id": "model",
"response_format": "format",
}
"""Maps ChatOptions keys to Ollama API parameter names."""
@@ -278,7 +277,7 @@ class OllamaSettings(TypedDict, total=False):
"""Ollama settings."""
host: str | None
model_id: str | None
model: str | None
logger = logging.getLogger("agent_framework.ollama")
@@ -299,7 +298,7 @@ class OllamaChatClient(
*,
host: str | None = None,
client: AsyncClient | None = None,
model_id: str | None = None,
model: str | None = None,
additional_properties: dict[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
@@ -312,7 +311,7 @@ class OllamaChatClient(
host: Ollama server URL, if none `http://localhost:11434` is used.
Can be set via the OLLAMA_HOST env variable.
client: An optional Ollama Client instance. If not provided, a new instance will be created.
model_id: The Ollama chat model ID to use. Can be set via the OLLAMA_MODEL_ID env variable.
model: The Ollama chat model to use. Can be set via the OLLAMA_MODEL env variable.
additional_properties: Additional properties stored on the client instance.
middleware: Optional middleware to apply to the client.
function_invocation_configuration: Optional function invocation configuration override.
@@ -322,14 +321,14 @@ class OllamaChatClient(
ollama_settings = load_settings(
OllamaSettings,
env_prefix="OLLAMA_",
required_fields=["model_id"],
required_fields=["model"],
host=host,
model_id=model_id,
model=model,
env_file_encoding=env_file_encoding,
env_file_path=env_file_path,
)
self.model_id = ollama_settings["model_id"] # type: ignore[assignment, reportTypedDictNotRequiredAccess]
self.model = ollama_settings["model"] # type: ignore[assignment, reportTypedDictNotRequiredAccess]
# we can just pass in None for the host, the default is set by the Ollama package.
self.client = client or AsyncClient(host=ollama_settings.get("host"))
# Save Host URL for serialization with to_dict()
@@ -411,7 +410,7 @@ class OllamaChatClient(
translated_key = OLLAMA_MODEL_OPTION_TRANSLATIONS.get(key, key)
model_options[translated_key] = value
else:
# Apply top-level translations (e.g., model_id -> model)
# Apply top-level translations (e.g., response_format -> format)
translated_key = OLLAMA_OPTION_TRANSLATIONS.get(key, key)
run_options[translated_key] = value
@@ -425,11 +424,11 @@ class OllamaChatClient(
if "messages" not in run_options:
raise ChatClientInvalidRequestException("Messages are required for chat completions")
# model id
# model
if not run_options.get("model"):
if not self.model_id:
raise ValueError("model_id must be a non-empty string")
run_options["model"] = self.model_id
if not self.model:
raise ValueError("model must be a non-empty string")
run_options["model"] = self.model
# tools
tools = options.get("tools")
@@ -533,7 +532,7 @@ class OllamaChatClient(
return ChatResponseUpdate(
contents=contents,
role="assistant",
model_id=response.model,
model=response.model,
created_at=response.created_at,
)
@@ -542,7 +541,7 @@ class OllamaChatClient(
return ChatResponse(
messages=[Message(role="assistant", contents=contents)],
model_id=response.model,
model=response.model,
created_at=response.created_at,
usage_details=UsageDetails(
input_token_count=response.prompt_eval_count,
@@ -38,7 +38,7 @@ class OllamaEmbeddingOptions(EmbeddingGenerationOptions, total=False):
from agent_framework_ollama import OllamaEmbeddingOptions
options: OllamaEmbeddingOptions = {
"model_id": "nomic-embed-text",
"model": "nomic-embed-text",
"dimensions": 768,
"truncate": True,
}
@@ -67,7 +67,7 @@ class OllamaEmbeddingSettings(TypedDict, total=False):
"""Ollama embedding settings."""
host: str | None
embedding_model_id: str | None
embedding_model: str | None
class RawOllamaEmbeddingClient(
@@ -77,8 +77,8 @@ class RawOllamaEmbeddingClient(
"""Raw Ollama embedding client without telemetry.
Keyword Args:
model_id: The Ollama embedding model ID (e.g. "nomic-embed-text").
Can also be set via environment variable OLLAMA_EMBEDDING_MODEL_ID.
model: The Ollama embedding model (e.g. "nomic-embed-text").
Can also be set via environment variable OLLAMA_EMBEDDING_MODEL.
host: Ollama server URL. Defaults to http://localhost:11434.
Can also be set via environment variable OLLAMA_HOST.
client: Optional pre-configured Ollama AsyncClient.
@@ -89,7 +89,7 @@ class RawOllamaEmbeddingClient(
def __init__(
self,
*,
model_id: str | None = None,
model: str | None = None,
host: str | None = None,
client: AsyncClient | None = None,
additional_properties: dict[str, Any] | None = None,
@@ -100,14 +100,14 @@ class RawOllamaEmbeddingClient(
ollama_settings = load_settings(
OllamaEmbeddingSettings,
env_prefix="OLLAMA_",
required_fields=["embedding_model_id"],
required_fields=["embedding_model"],
host=host,
embedding_model_id=model_id,
embedding_model=model,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
self.model_id = ollama_settings["embedding_model_id"] # type: ignore[assignment,reportTypedDictNotRequiredAccess]
self.model = ollama_settings["embedding_model"] # type: ignore[assignment,reportTypedDictNotRequiredAccess]
self.client = client or AsyncClient(host=ollama_settings.get("host"))
self.host = str(self.client._client.base_url) # type: ignore[reportUnknownMemberType,reportPrivateUsage,reportUnknownArgumentType]
super().__init__(additional_properties=additional_properties)
@@ -132,15 +132,15 @@ class RawOllamaEmbeddingClient(
Generated embeddings with usage metadata.
Raises:
ValueError: If model_id is not provided or values is empty.
ValueError: If model is not provided or values is empty.
"""
if not values:
return GeneratedEmbeddings([], options=options)
opts: dict[str, Any] = options or {} # type: ignore
model = opts.get("model_id") or self.model_id
model = opts.get("model") or self.model
if not model:
raise ValueError("model_id is required")
raise ValueError("model is required")
kwargs: dict[str, Any] = {"model": model, "input": list(values)}
if (truncate := opts.get("truncate")) is not None:
@@ -156,7 +156,7 @@ class RawOllamaEmbeddingClient(
Embedding(
vector=list(emb),
dimensions=len(emb),
model_id=response.get("model") or model, # type: ignore[assignment]
model=response.get("model") or model, # type: ignore[assignment]
)
for emb in response.get("embeddings", [])
]
@@ -177,8 +177,8 @@ class OllamaEmbeddingClient(
"""Ollama embedding client with telemetry support.
Keyword Args:
model_id: The Ollama embedding model ID (e.g. "nomic-embed-text").
Can also be set via environment variable OLLAMA_EMBEDDING_MODEL_ID.
model: The Ollama embedding model (e.g. "nomic-embed-text").
Can also be set via environment variable OLLAMA_EMBEDDING_MODEL.
host: Ollama server URL. Defaults to http://localhost:11434.
Can also be set via environment variable OLLAMA_HOST.
client: Optional pre-configured Ollama AsyncClient.
@@ -191,12 +191,12 @@ class OllamaEmbeddingClient(
from agent_framework_ollama import OllamaEmbeddingClient
# Using environment variables
# Set OLLAMA_EMBEDDING_MODEL_ID=nomic-embed-text
# Set OLLAMA_EMBEDDING_MODEL=nomic-embed-text
client = OllamaEmbeddingClient()
# Or passing parameters directly
client = OllamaEmbeddingClient(
model_id="nomic-embed-text",
model="nomic-embed-text",
host="http://localhost:11434",
)
@@ -210,7 +210,7 @@ class OllamaEmbeddingClient(
def __init__(
self,
*,
model_id: str | None = None,
model: str | None = None,
host: str | None = None,
client: AsyncClient | None = None,
otel_provider_name: str | None = None,
@@ -220,7 +220,7 @@ class OllamaEmbeddingClient(
) -> None:
"""Initialize an Ollama embedding client."""
super().__init__(
model_id=model_id,
model=model,
host=host,
client=client,
additional_properties=additional_properties,
@@ -13,11 +13,11 @@ from agent_framework_ollama import OllamaEmbeddingClient, OllamaEmbeddingOptions
def test_ollama_embedding_construction(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test construction with explicit parameters."""
monkeypatch.setenv("OLLAMA_EMBEDDING_MODEL_ID", "nomic-embed-text")
monkeypatch.setenv("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text")
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client_cls.return_value = MagicMock()
client = OllamaEmbeddingClient()
assert client.model_id == "nomic-embed-text"
assert client.model == "nomic-embed-text"
def test_ollama_embedding_construction_with_params() -> None:
@@ -25,16 +25,16 @@ def test_ollama_embedding_construction_with_params() -> None:
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client_cls.return_value = MagicMock()
client = OllamaEmbeddingClient(
model_id="nomic-embed-text",
model="nomic-embed-text",
host="http://localhost:11434",
)
assert client.model_id == "nomic-embed-text"
assert client.model == "nomic-embed-text"
def test_ollama_embedding_construction_missing_model_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that missing model_id raises an error."""
monkeypatch.delenv("OLLAMA_EMBEDDING_MODEL_ID", raising=False)
monkeypatch.delenv("OLLAMA_MODEL_ID", raising=False)
"""Test that missing model raises an error."""
monkeypatch.delenv("OLLAMA_EMBEDDING_MODEL", raising=False)
monkeypatch.delenv("OLLAMA_MODEL", raising=False)
from agent_framework.exceptions import SettingNotFoundError
with pytest.raises(SettingNotFoundError):
@@ -54,14 +54,14 @@ async def test_ollama_embedding_get_embeddings() -> None:
mock_client.embed = AsyncMock(return_value=mock_response)
mock_client_cls.return_value = mock_client
client = OllamaEmbeddingClient(model_id="nomic-embed-text")
client = OllamaEmbeddingClient(model="nomic-embed-text")
result = await client.get_embeddings(["hello", "world"])
assert isinstance(result, GeneratedEmbeddings)
assert len(result) == 2
assert result[0].vector == [0.1, 0.2, 0.3]
assert result[1].vector == [0.4, 0.5, 0.6]
assert result[0].model_id == "nomic-embed-text"
assert result[0].model == "nomic-embed-text"
assert result.usage == {"input_token_count": 10}
mock_client.embed.assert_called_once_with(
@@ -76,7 +76,7 @@ async def test_ollama_embedding_get_embeddings_empty_input() -> None:
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
client = OllamaEmbeddingClient(model_id="nomic-embed-text")
client = OllamaEmbeddingClient(model="nomic-embed-text")
result = await client.get_embeddings([])
assert isinstance(result, GeneratedEmbeddings)
@@ -96,7 +96,7 @@ async def test_ollama_embedding_get_embeddings_with_options() -> None:
mock_client.embed = AsyncMock(return_value=mock_response)
mock_client_cls.return_value = mock_client
client = OllamaEmbeddingClient(model_id="nomic-embed-text")
client = OllamaEmbeddingClient(model="nomic-embed-text")
options: OllamaEmbeddingOptions = {
"truncate": True,
"dimensions": 512,
@@ -113,22 +113,22 @@ async def test_ollama_embedding_get_embeddings_with_options() -> None:
async def test_ollama_embedding_get_embeddings_no_model_raises() -> None:
"""Test that missing model_id at call time raises ValueError."""
"""Test that missing model at call time raises ValueError."""
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
client = OllamaEmbeddingClient(model_id="nomic-embed-text")
client.model_id = None # type: ignore[assignment]
client = OllamaEmbeddingClient(model="nomic-embed-text")
client.model = None # type: ignore[assignment]
with pytest.raises(ValueError, match="model_id is required"):
with pytest.raises(ValueError, match="model is required"):
await client.get_embeddings(["hello"])
# region: Integration Tests
skip_if_ollama_embedding_integration_tests_disabled = pytest.mark.skipif(
os.getenv("OLLAMA_EMBEDDING_MODEL_ID", "") in ("", "test-model"),
os.getenv("OLLAMA_EMBEDDING_MODEL", "") in ("", "test-model"),
reason="No real Ollama embedding model provided; skipping integration tests.",
)
@@ -26,7 +26,7 @@ from agent_framework_ollama import OllamaChatClient
# region Service Setup
skip_if_azure_integration_tests_disabled = pytest.mark.skipif(
os.getenv("OLLAMA_MODEL_ID", "") in ("", "test-model"),
os.getenv("OLLAMA_MODEL", "") in ("", "test-model"),
reason="No real Ollama chat model provided; skipping integration tests.",
)
@@ -55,7 +55,7 @@ def ollama_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): #
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {"OLLAMA_HOST": "http://localhost:12345", "OLLAMA_MODEL_ID": "test"}
env_vars = {"OLLAMA_HOST": "http://localhost:12345", "OLLAMA_MODEL": "test"}
env_vars.update(override_env_param_dict) # type: ignore
@@ -156,7 +156,7 @@ def test_init(ollama_unit_test_env: dict[str, str]) -> None:
assert ollama_chat_client.client is not None
assert isinstance(ollama_chat_client.client, AsyncClient)
assert ollama_chat_client.model_id == ollama_unit_test_env["OLLAMA_MODEL_ID"]
assert ollama_chat_client.model == ollama_unit_test_env["OLLAMA_MODEL"]
assert isinstance(ollama_chat_client, BaseChatClient)
@@ -165,27 +165,27 @@ def test_init_client(ollama_unit_test_env: dict[str, str]) -> None:
test_client = MagicMock(spec=AsyncClient)
# Mock underlying HTTP client's base_url
test_client._client = MagicMock()
test_client._client.base_url = ollama_unit_test_env["OLLAMA_MODEL_ID"]
test_client._client.base_url = ollama_unit_test_env["OLLAMA_MODEL"]
ollama_chat_client = OllamaChatClient(client=test_client)
assert ollama_chat_client.client is test_client
assert ollama_chat_client.model_id == ollama_unit_test_env["OLLAMA_MODEL_ID"]
assert ollama_chat_client.model == ollama_unit_test_env["OLLAMA_MODEL"]
assert isinstance(ollama_chat_client, BaseChatClient)
@pytest.mark.parametrize("exclude_list", [["OLLAMA_MODEL_ID"]], indirect=True)
@pytest.mark.parametrize("exclude_list", [["OLLAMA_MODEL"]], indirect=True)
def test_with_invalid_settings(ollama_unit_test_env: dict[str, str]) -> None:
with pytest.raises(SettingNotFoundError, match="Required setting 'model_id'"):
with pytest.raises(SettingNotFoundError, match="Required setting 'model'"):
OllamaChatClient(
host="http://localhost:12345",
model_id=None,
model=None,
)
def test_serialize(ollama_unit_test_env: dict[str, str]) -> None:
settings = {
"host": ollama_unit_test_env["OLLAMA_HOST"],
"model_id": ollama_unit_test_env["OLLAMA_MODEL_ID"],
"model": ollama_unit_test_env["OLLAMA_MODEL"],
}
ollama_chat_client = OllamaChatClient.from_dict(settings)
@@ -193,7 +193,7 @@ def test_serialize(ollama_unit_test_env: dict[str, str]) -> None:
assert isinstance(serialized, dict)
assert serialized["host"] == ollama_unit_test_env["OLLAMA_HOST"]
assert serialized["model_id"] == ollama_unit_test_env["OLLAMA_MODEL_ID"]
assert serialized["model"] == ollama_unit_test_env["OLLAMA_MODEL"]
def test_chat_middleware(ollama_unit_test_env: dict[str, str]) -> None:
@@ -225,7 +225,7 @@ def test_additional_properties(ollama_unit_test_env: dict[str, str]) -> None:
async def test_empty_messages() -> None:
ollama_chat_client = OllamaChatClient(
host="http://localhost:12345",
model_id="test-model",
model="test-model",
)
with pytest.raises(ChatClientInvalidRequestException):
await ollama_chat_client.get_response(messages=[])
+7 -7
View File
@@ -56,16 +56,16 @@ These variables are used when the client is configured for Azure OpenAI:
| `AZURE_OPENAI_BASE_URL` | Full Azure OpenAI base URL (`.../openai/v1`) |
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key |
| `AZURE_OPENAI_API_VERSION` | Azure OpenAI API version |
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Generic fallback deployment |
| `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME` | Preferred deployment for `OpenAIChatClient` |
| `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME` | Preferred deployment for `OpenAIChatCompletionClient` |
| `AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME` | Preferred deployment for `OpenAIEmbeddingClient` |
| `AZURE_OPENAI_MODEL` | Generic fallback deployment |
| `AZURE_OPENAI_RESPONSES_MODEL` | Preferred deployment for `OpenAIChatClient` |
| `AZURE_OPENAI_CHAT_MODEL` | Preferred deployment for `OpenAIChatCompletionClient` |
| `AZURE_OPENAI_EMBEDDING_MODEL` | Preferred deployment for `OpenAIEmbeddingClient` |
Deployment lookup order:
- `OpenAIChatClient`: `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME` -> `AZURE_OPENAI_DEPLOYMENT_NAME`
- `OpenAIChatCompletionClient`: `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME` -> `AZURE_OPENAI_DEPLOYMENT_NAME`
- `OpenAIEmbeddingClient`: `AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME` -> `AZURE_OPENAI_DEPLOYMENT_NAME`
- `OpenAIChatClient`: `AZURE_OPENAI_RESPONSES_MODEL` -> `AZURE_OPENAI_MODEL`
- `OpenAIChatCompletionClient`: `AZURE_OPENAI_CHAT_MODEL` -> `AZURE_OPENAI_MODEL`
- `OpenAIEmbeddingClient`: `AZURE_OPENAI_EMBEDDING_MODEL` -> `AZURE_OPENAI_MODEL`
When both OpenAI and Azure environment variables are present, the generic clients prefer OpenAI
when `OPENAI_API_KEY` is configured. To use Azure explicitly, pass `azure_endpoint` or
@@ -331,8 +331,8 @@ class RawOpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME``.
reads ``AZURE_OPENAI_RESPONSES_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
credential: Azure credential or token provider for Entra auth.
@@ -361,7 +361,6 @@ class RawOpenAIChatClient( # type: ignore[misc]
self,
model: str | None = None,
*,
model_id: str | None = None,
api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None,
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
org_id: str | None = None,
@@ -382,9 +381,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_RESPONSES_MODEL`` and then ``OPENAI_MODEL`` for OpenAI,
or ``AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME`` for Azure.
model_id: Deprecated alias for ``model``.
or ``AZURE_OPENAI_RESPONSES_MODEL`` and then ``AZURE_OPENAI_MODEL`` for Azure.
api_key: API key override. For OpenAI this maps to ``OPENAI_API_KEY``.
For Azure this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
auth. A callable token provider is also accepted for backwards compatibility,
@@ -424,15 +421,9 @@ class RawOpenAIChatClient( # type: ignore[misc]
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_RESPONSES_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure routing
reads ``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME``,
``AZURE_OPENAI_DEPLOYMENT_NAME``, and ``AZURE_OPENAI_API_VERSION``.
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_RESPONSES_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
"""
if model_id is not None and model is None:
import warnings
warnings.warn("model_id is deprecated, use model instead", DeprecationWarning, stacklevel=2)
model = model_id
settings, client, use_azure_client = load_openai_service_settings(
model=model,
api_key=api_key,
@@ -447,12 +438,12 @@ class RawOpenAIChatClient( # type: ignore[misc]
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
openai_model_fields=("responses_model", "model"),
azure_deployment_fields=("responses_deployment_name", "deployment_name"),
azure_model_fields=("responses_model", "model"),
responses_mode=True,
)
self.client = client
self.model: str = settings.get("model") or settings.get("deployment_name") or ""
self.model: str = settings.get("model") or ""
# Store configuration for serialization
self.org_id = settings.get("org_id")
@@ -1178,7 +1169,6 @@ class RawOpenAIChatClient( # type: ignore[misc]
# translations between options and Responses API
translations = {
"model_id": "model", # backward compat: accept model_id in options
"allow_multiple_tool_calls": "parallel_tool_calls",
"conversation_id": "previous_response_id",
"max_tokens": "max_output_tokens",
@@ -2559,8 +2549,8 @@ class OpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME``.
reads ``AZURE_OPENAI_RESPONSES_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
credential: Azure credential or token provider for Entra auth.
@@ -2613,8 +2603,8 @@ class OpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_RESPONSES_MODEL`` and then ``OPENAI_MODEL`` for OpenAI
routing, or ``AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME`` for Azure routing.
routing, or ``AZURE_OPENAI_RESPONSES_MODEL`` and then
``AZURE_OPENAI_MODEL`` for Azure routing.
api_key: API key override. For OpenAI routing this maps to ``OPENAI_API_KEY``.
For Azure routing this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
auth. A callable token provider is also accepted for backwards compatibility,
@@ -2656,8 +2646,8 @@ class OpenAIChatClient( # type: ignore[misc]
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_RESPONSES_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure routing
reads ``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME``,
``AZURE_OPENAI_DEPLOYMENT_NAME``, and ``AZURE_OPENAI_API_VERSION``.
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_RESPONSES_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
Examples:
.. code-block:: python
@@ -114,7 +114,7 @@ class OpenAIChatCompletionOptions(ChatOptions[ResponseModelT], Generic[ResponseM
Extends ChatOptions with options specific to OpenAI's Chat Completions API.
Keys:
model_id: The model to use for the request,
model: The model to use for the request,
translates to ``model`` in OpenAI API.
temperature: Sampling temperature between 0 and 2.
top_p: Nucleus sampling parameter.
@@ -155,7 +155,6 @@ OpenAIChatCompletionOptionsT = TypeVar(
)
OPTION_TRANSLATIONS: dict[str, str] = {
"model_id": "model", # backward compat: accept model_id in options
"allow_multiple_tool_calls": "parallel_tool_calls",
"max_tokens": "max_completion_tokens",
}
@@ -246,8 +245,8 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME``.
reads ``AZURE_OPENAI_CHAT_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
credential: Azure credential or token provider for Entra auth.
@@ -276,7 +275,6 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
self,
model: str | None = None,
*,
model_id: str | None = None,
api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None,
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
org_id: str | None = None,
@@ -297,9 +295,7 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL`` for OpenAI routing,
or ``AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME`` for Azure routing.
model_id: Deprecated alias for ``model``.
or ``AZURE_OPENAI_CHAT_MODEL`` and then ``AZURE_OPENAI_MODEL`` for Azure routing.
api_key: API key override. For OpenAI routing this maps to ``OPENAI_API_KEY``.
For Azure routing this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
auth. A callable token provider is also accepted for backwards compatibility,
@@ -339,15 +335,9 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_CHAT_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure routing
reads ``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_DEPLOYMENT_NAME``,
``AZURE_OPENAI_DEPLOYMENT_NAME``, and ``AZURE_OPENAI_API_VERSION``.
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
"""
if model_id is not None and model is None:
import warnings
warnings.warn("model_id is deprecated, use model instead", DeprecationWarning, stacklevel=2)
model = model_id
settings, client, use_azure_client = load_openai_service_settings(
model=model,
api_key=api_key,
@@ -362,11 +352,11 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
openai_model_fields=("chat_model", "model"),
azure_deployment_fields=("chat_deployment_name", "deployment_name"),
azure_model_fields=("chat_model", "model"),
)
self.client = client
self.model: str = settings.get("model") or settings.get("deployment_name") or ""
self.model: str = settings.get("model") or ""
# Store configuration for serialization
self.org_id = settings.get("org_id")
@@ -1098,8 +1088,8 @@ class OpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME``.
reads ``AZURE_OPENAI_CHAT_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
credential: Azure credential or token provider for Entra auth.
@@ -1146,8 +1136,8 @@ class OpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL`` for OpenAI routing,
or ``AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME`` for Azure routing.
or ``AZURE_OPENAI_CHAT_MODEL`` and then
``AZURE_OPENAI_MODEL`` for Azure routing.
api_key: API key override. For OpenAI routing this maps to ``OPENAI_API_KEY``.
For Azure routing this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
auth. A callable token provider is also accepted for backwards compatibility,
@@ -1186,8 +1176,8 @@ class OpenAIChatCompletionClient( # type: ignore[misc]
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_CHAT_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure routing
reads ``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_DEPLOYMENT_NAME``,
``AZURE_OPENAI_DEPLOYMENT_NAME``, and ``AZURE_OPENAI_API_VERSION``.
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
Examples:
.. code-block:: python
@@ -123,8 +123,8 @@ class RawOpenAIEmbeddingClient(
Keyword Args:
model: Embedding deployment name. When not provided, the constructor reads
``AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME``.
``AZURE_OPENAI_EMBEDDING_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
credential: Azure credential or token provider for Entra auth.
@@ -150,7 +150,6 @@ class RawOpenAIEmbeddingClient(
self,
*,
model: str | None = None,
model_id: str | None = None,
api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None,
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
org_id: str | None = None,
@@ -168,9 +167,8 @@ class RawOpenAIEmbeddingClient(
Keyword Args:
model: Embedding model or Azure OpenAI deployment name. When not provided, the
constructor reads ``OPENAI_EMBEDDING_MODEL`` and then ``OPENAI_MODEL``
for OpenAI. For Azure it first checks ``AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME``
and then ``AZURE_OPENAI_DEPLOYMENT_NAME``.
model_id: Deprecated alias for ``model``.
for OpenAI. For Azure it first checks ``AZURE_OPENAI_EMBEDDING_MODEL``
and then ``AZURE_OPENAI_MODEL``.
api_key: API key override. For OpenAI this maps to ``OPENAI_API_KEY``.
For Azure this can be used instead of ``AZURE_OPENAI_API_KEY`` for key auth.
A callable token provider is also accepted for backwards compatibility,
@@ -207,15 +205,9 @@ class RawOpenAIEmbeddingClient(
OpenAI reads ``OPENAI_API_KEY``, ``OPENAI_EMBEDDING_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure reads
``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME``,
``AZURE_OPENAI_DEPLOYMENT_NAME``, and ``AZURE_OPENAI_API_VERSION``.
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_EMBEDDING_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
"""
if model_id is not None and model is None:
import warnings
warnings.warn("model_id is deprecated, use model instead", DeprecationWarning, stacklevel=2)
model = model_id
settings, client, use_azure_client = load_openai_service_settings(
model=model,
api_key=api_key,
@@ -230,11 +222,11 @@ class RawOpenAIEmbeddingClient(
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
openai_model_fields=("embedding_model", "model"),
azure_deployment_fields=("embedding_deployment_name", "deployment_name"),
azure_model_fields=("embedding_model", "model"),
)
self.client = client
resolved_model = settings.get("model") or settings.get("deployment_name")
resolved_model = settings.get("model")
self.model: str | None = resolved_model.strip() if isinstance(resolved_model, str) and resolved_model else None
# Store configuration for serialization
@@ -279,8 +271,7 @@ class RawOpenAIEmbeddingClient(
return GeneratedEmbeddings([], options=options) # type: ignore
opts: dict[str, Any] = options or {} # type: ignore
# backward compat: accept model_id in options
model = opts.get("model") or opts.get("model_id") or self.model
model = opts.get("model") or self.model
if not model:
raise ValueError("model is required")
@@ -385,8 +376,8 @@ class OpenAIEmbeddingClient(
Keyword Args:
model: Embedding deployment name. When not provided, the constructor reads
``AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME`` and then
``AZURE_OPENAI_DEPLOYMENT_NAME``.
``AZURE_OPENAI_EMBEDDING_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
credential: Azure credential or token provider for Entra auth.
@@ -429,8 +420,8 @@ class OpenAIEmbeddingClient(
Keyword Args:
model: Embedding model or Azure OpenAI deployment name. When not provided, the
constructor reads ``OPENAI_EMBEDDING_MODEL`` and then ``OPENAI_MODEL``
for OpenAI. For Azure it first checks ``AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME``
and then ``AZURE_OPENAI_DEPLOYMENT_NAME``.
for OpenAI. For Azure it first checks ``AZURE_OPENAI_EMBEDDING_MODEL``
and then ``AZURE_OPENAI_MODEL``.
api_key: API key override. For OpenAI this maps to ``OPENAI_API_KEY``.
For Azure this can be used instead of ``AZURE_OPENAI_API_KEY`` for key auth.
A callable token provider is also accepted for backwards compatibility,
@@ -467,8 +458,8 @@ class OpenAIEmbeddingClient(
OpenAI reads ``OPENAI_API_KEY``, ``OPENAI_EMBEDDING_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure reads
``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME``,
``AZURE_OPENAI_DEPLOYMENT_NAME``, and ``AZURE_OPENAI_API_VERSION``.
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_EMBEDDING_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
Examples:
.. code-block:: python
@@ -104,17 +104,14 @@ class AzureOpenAISettings(TypedDict, total=False):
endpoint: str | None
base_url: str | None
api_key: SecretString | None
deployment_name: str | None
embedding_deployment_name: str | None
chat_deployment_name: str | None
responses_deployment_name: str | None
model: str | None
embedding_model: str | None
chat_model: str | None
responses_model: str | None
api_version: str | None
OpenAIModelSettingName = Literal["model", "embedding_model", "chat_model", "responses_model"]
AzureDeploymentSettingName = Literal[
"deployment_name", "embedding_deployment_name", "chat_deployment_name", "responses_deployment_name"
]
OPENAI_MODEL_ENV_VARS: dict[OpenAIModelSettingName, str] = {
"model": "OPENAI_MODEL",
@@ -123,17 +120,17 @@ OPENAI_MODEL_ENV_VARS: dict[OpenAIModelSettingName, str] = {
"responses_model": "OPENAI_RESPONSES_MODEL",
}
AZURE_DEPLOYMENT_ENV_VARS: dict[AzureDeploymentSettingName, str] = {
"deployment_name": "AZURE_OPENAI_DEPLOYMENT_NAME",
"embedding_deployment_name": "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME",
"chat_deployment_name": "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME",
"responses_deployment_name": "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME",
AZURE_MODEL_ENV_VARS: dict[OpenAIModelSettingName, str] = {
"model": "AZURE_OPENAI_MODEL",
"embedding_model": "AZURE_OPENAI_EMBEDDING_MODEL",
"chat_model": "AZURE_OPENAI_CHAT_MODEL",
"responses_model": "AZURE_OPENAI_RESPONSES_MODEL",
}
def _resolve_named_setting(
settings: Mapping[str, Any],
fields: Sequence[OpenAIModelSettingName | AzureDeploymentSettingName],
fields: Sequence[OpenAIModelSettingName],
) -> str | None:
"""Return the first populated value from ``fields``."""
for field in fields:
@@ -163,10 +160,10 @@ def load_openai_service_settings(
env_file_path: str | None,
env_file_encoding: str | None,
openai_model_fields: Sequence[OpenAIModelSettingName] = ("model",),
azure_deployment_fields: Sequence[AzureDeploymentSettingName] = ("deployment_name",),
azure_model_fields: Sequence[OpenAIModelSettingName] = ("model",),
responses_mode: bool = False,
) -> tuple[dict[str, Any], AsyncOpenAI, bool]:
"""Load OpenAI settings, including Azure OpenAI aliases.
"""Load OpenAI settings, including Azure OpenAI model aliases.
The generic OpenAI clients primarily read from ``OPENAI_*`` variables. Azure-specific
environment variables are used only when an explicit Azure signal is present
@@ -235,20 +232,18 @@ def load_openai_service_settings(
env_file_encoding=env_file_encoding,
)
if model is not None:
azure_settings[azure_deployment_fields[0]] = model
azure_settings[azure_model_fields[0]] = model
client_args = {}
resolved_azure_deployment = _resolve_named_setting(azure_settings, azure_deployment_fields)
if resolved_azure_deployment is None and client:
resolved_azure_model = _resolve_named_setting(azure_settings, azure_model_fields)
if resolved_azure_model is None and client:
azure_deployment = getattr(client, "_azure_deployment", None)
if isinstance(azure_deployment, str) and azure_deployment:
resolved_azure_deployment = azure_deployment
if resolved_azure_deployment:
azure_settings["deployment_name"] = resolved_azure_deployment
client_args["azure_deployment"] = resolved_azure_deployment
resolved_azure_model = azure_deployment
if resolved_azure_model:
azure_settings["model"] = resolved_azure_model
client_args["azure_deployment"] = resolved_azure_model
else:
deployment_env_guidance = _join_env_names([
AZURE_DEPLOYMENT_ENV_VARS[field] for field in azure_deployment_fields
])
deployment_env_guidance = _join_env_names([AZURE_MODEL_ENV_VARS[field] for field in azure_model_fields])
has_azure_configuration = (
client is not None
or azure_settings.get("endpoint") is not None
@@ -261,7 +256,7 @@ def load_openai_service_settings(
"'AZURE_OPENAI_BASE_URL'."
)
raise SettingNotFoundError(
"Azure OpenAI client requires a deployment name, which can be provided via the 'model' parameter, "
"Azure OpenAI client requires a model, which can be provided via the 'model' parameter, "
f"or the {deployment_env_guidance} environment variable."
)
if client:
+14 -29
View File
@@ -45,20 +45,15 @@ def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): #
"OPENAI_EMBEDDING_MODEL",
"OPENAI_CHAT_MODEL",
"OPENAI_RESPONSES_MODEL",
"OPENAI_TEXT_MODEL_ID",
"OPENAI_TEXT_TO_IMAGE_MODEL_ID",
"OPENAI_AUDIO_TO_TEXT_MODEL_ID",
"OPENAI_TEXT_TO_AUDIO_MODEL_ID",
"OPENAI_REALTIME_MODEL_ID",
"OPENAI_API_VERSION",
"OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME",
"AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME",
"AZURE_OPENAI_DEPLOYMENT_NAME",
"AZURE_OPENAI_CHAT_MODEL",
"AZURE_OPENAI_RESPONSES_MODEL",
"AZURE_OPENAI_EMBEDDING_MODEL",
"AZURE_OPENAI_MODEL",
"AZURE_OPENAI_API_VERSION",
],
)
@@ -66,13 +61,8 @@ def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): #
env_vars = {
"OPENAI_API_KEY": "test-dummy-key",
"OPENAI_ORG_ID": "test_org_id",
"OPENAI_MODEL": "test_model_id",
"OPENAI_EMBEDDING_MODEL": "test_embedding_model_id",
"OPENAI_TEXT_MODEL_ID": "test_text_model_id",
"OPENAI_TEXT_TO_IMAGE_MODEL_ID": "test_text_to_image_model_id",
"OPENAI_AUDIO_TO_TEXT_MODEL_ID": "test_audio_to_text_model_id",
"OPENAI_TEXT_TO_AUDIO_MODEL_ID": "test_text_to_audio_model_id",
"OPENAI_REALTIME_MODEL_ID": "test_realtime_model_id",
"OPENAI_MODEL": "test_model",
"OPENAI_EMBEDDING_MODEL": "test_embedding_model",
}
env_vars.update(override_env_param_dict) # type: ignore
@@ -104,30 +94,25 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic
"OPENAI_EMBEDDING_MODEL",
"OPENAI_CHAT_MODEL",
"OPENAI_RESPONSES_MODEL",
"OPENAI_TEXT_MODEL_ID",
"OPENAI_TEXT_TO_IMAGE_MODEL_ID",
"OPENAI_AUDIO_TO_TEXT_MODEL_ID",
"OPENAI_TEXT_TO_AUDIO_MODEL_ID",
"OPENAI_REALTIME_MODEL_ID",
"OPENAI_API_VERSION",
"OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME",
"AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME",
"AZURE_OPENAI_DEPLOYMENT_NAME",
"AZURE_OPENAI_CHAT_MODEL",
"AZURE_OPENAI_RESPONSES_MODEL",
"AZURE_OPENAI_EMBEDDING_MODEL",
"AZURE_OPENAI_MODEL",
"AZURE_OPENAI_API_VERSION",
],
)
env_vars = {
"AZURE_OPENAI_ENDPOINT": "https://test-endpoint.openai.azure.com",
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "test_chat_deployment",
"AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME": "test_responses_deployment",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME": "test_embedding_deployment",
"AZURE_OPENAI_DEPLOYMENT_NAME": "test_deployment",
"AZURE_OPENAI_CHAT_MODEL": "test_chat_deployment",
"AZURE_OPENAI_RESPONSES_MODEL": "test_responses_deployment",
"AZURE_OPENAI_EMBEDDING_MODEL": "test_embedding_deployment",
"AZURE_OPENAI_MODEL": "test_deployment",
"AZURE_OPENAI_API_KEY": "test_api_key",
"AZURE_OPENAI_API_VERSION": "2024-12-01-preview",
}
@@ -151,11 +151,11 @@ def test_openai_chat_client_tool_methods_return_dict() -> None:
def test_init_prefers_openai_responses_model(monkeypatch, openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_RESPONSES_MODEL", "test_responses_model_id")
monkeypatch.setenv("OPENAI_RESPONSES_MODEL", "test_responses_model")
openai_responses_client = OpenAIChatClient()
assert openai_responses_client.model == "test_responses_model_id"
assert openai_responses_client.model == "test_responses_model"
def test_init_validation_fail() -> None:
@@ -164,12 +164,12 @@ def test_init_validation_fail() -> None:
OpenAIChatClient(api_key="34523", model={"test": "dict"}) # type: ignore
def test_init_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None:
def test_init_model_constructor(openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
model_id = "test_model_id"
openai_responses_client = OpenAIChatClient(model=model_id)
model = "test_model"
openai_responses_client = OpenAIChatClient(model=model)
assert openai_responses_client.model == model_id
assert openai_responses_client.model == model
assert isinstance(openai_responses_client, SupportsChatGetResponse)
@@ -191,18 +191,18 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None:
@pytest.mark.parametrize("exclude_list", [["OPENAI_MODEL"]], indirect=True)
def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None:
def test_init_with_empty_model(openai_unit_test_env: dict[str, str]) -> None:
with pytest.raises(SettingNotFoundError):
OpenAIChatClient()
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None:
model_id = "test_model_id"
model = "test_model"
with pytest.raises(SettingNotFoundError):
OpenAIChatClient(
model=model_id,
model=model,
)
@@ -24,10 +24,7 @@ pytestmark = pytest.mark.azure
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.openai.azure.com")
or (
os.getenv("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "") == ""
and os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "") == ""
),
or (os.getenv("AZURE_OPENAI_RESPONSES_MODEL", "") == "" and os.getenv("AZURE_OPENAI_MODEL", "") == ""),
reason="No real Azure OpenAI endpoint or responses deployment provided; skipping integration tests.",
)
@@ -39,9 +36,7 @@ def _with_azure_openai_debug() -> Any:
try:
return await func(*args, **kwargs)
except Exception as exc:
model = os.getenv("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME") or os.getenv(
"AZURE_OPENAI_DEPLOYMENT_NAME", "<unset>"
)
model = os.getenv("AZURE_OPENAI_RESPONSES_MODEL") or os.getenv("AZURE_OPENAI_MODEL", "<unset>")
api_version = os.getenv("AZURE_OPENAI_API_VERSION") or "preview"
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT", "<unset>")
debug_message = f"Azure OpenAI debug: endpoint={endpoint}, model={model}, api_version={api_version}"
@@ -102,7 +97,7 @@ async def get_weather(location: str) -> str:
def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatClient(credential=AzureCliCredential())
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_MODEL"]
assert isinstance(client, SupportsChatGetResponse)
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
@@ -112,7 +107,7 @@ def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) ->
def test_init_auto_detects_azure_env(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatClient()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -147,7 +142,7 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
client = OpenAIChatClient(credential=lambda: "token")
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -155,34 +150,34 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
def test_init_falls_back_to_generic_azure_deployment_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_MODEL", raising=False)
client = OpenAIChatClient()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
def test_init_does_not_fall_back_to_openai_responses_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.setenv("OPENAI_RESPONSES_MODEL", "test_responses_model")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a deployment name"):
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIChatClient()
def test_init_does_not_fall_back_to_openai_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.delenv("OPENAI_RESPONSES_MODEL", raising=False)
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a deployment name"):
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIChatClient()
@@ -209,7 +204,7 @@ def test_init_with_credential_wraps_async_token_credential(
def test_init_uses_default_azure_api_version(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatClient(credential=AzureCliCredential())
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_MODEL"]
assert client.api_version is not None
@@ -79,11 +79,11 @@ def test_supports_web_search_only() -> None:
def test_init_prefers_openai_chat_model(monkeypatch, openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_CHAT_MODEL", "test_chat_model_id")
monkeypatch.setenv("OPENAI_CHAT_MODEL", "test_chat_model")
open_ai_chat_completion = OpenAIChatCompletionClient()
assert open_ai_chat_completion.model == "test_chat_model_id"
assert open_ai_chat_completion.model == "test_chat_model"
def test_init_validation_fail() -> None:
@@ -92,12 +92,12 @@ def test_init_validation_fail() -> None:
OpenAIChatCompletionClient(api_key="34523", model={"test": "dict"}) # type: ignore
def test_init_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None:
def test_init_model_constructor(openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
model_id = "test_model_id"
open_ai_chat_completion = OpenAIChatCompletionClient(model=model_id)
model = "test_model"
open_ai_chat_completion = OpenAIChatCompletionClient(model=model)
assert open_ai_chat_completion.model == model_id
assert open_ai_chat_completion.model == model
assert isinstance(open_ai_chat_completion, SupportsChatGetResponse)
@@ -141,18 +141,18 @@ def test_init_base_url_from_settings_env() -> None:
@pytest.mark.parametrize("exclude_list", [["OPENAI_MODEL"]], indirect=True)
def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None:
def test_init_with_empty_model(openai_unit_test_env: dict[str, str]) -> None:
with pytest.raises(SettingNotFoundError):
OpenAIChatCompletionClient()
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None:
model_id = "test_model_id"
model = "test_model"
with pytest.raises(SettingNotFoundError):
OpenAIChatCompletionClient(
model=model_id,
model=model,
)
@@ -1178,10 +1178,10 @@ def test_parse_text_with_refusal(openai_unit_test_env: dict[str, str]) -> None:
assert message.contents[0].text == "I cannot provide that information."
def test_prepare_options_without_model_id(openai_unit_test_env: dict[str, str]) -> None:
"""Test that prepare_options raises error when model_id is not set."""
def test_prepare_options_without_model(openai_unit_test_env: dict[str, str]) -> None:
"""Test that prepare_options raises error when model is not set."""
client = OpenAIChatCompletionClient()
client.model = None # Remove model_id
client.model = None # Remove model
messages = [Message(role="user", text="test")]
@@ -29,9 +29,7 @@ pytestmark = pytest.mark.azure
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.openai.azure.com")
or (
os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "") == "" and os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "") == ""
),
or (os.getenv("AZURE_OPENAI_CHAT_MODEL", "") == "" and os.getenv("AZURE_OPENAI_MODEL", "") == ""),
reason="No real Azure OpenAI endpoint or chat deployment provided; skipping integration tests.",
)
@@ -43,9 +41,7 @@ def _with_azure_openai_debug() -> Any:
try:
return await func(*args, **kwargs)
except Exception as exc:
model = os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME") or os.getenv(
"AZURE_OPENAI_DEPLOYMENT_NAME", "<unset>"
)
model = os.getenv("AZURE_OPENAI_CHAT_MODEL") or os.getenv("AZURE_OPENAI_MODEL", "<unset>")
api_version = os.getenv("AZURE_OPENAI_API_VERSION", "<unset>")
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT", "<unset>")
debug_message = f"Azure OpenAI debug: endpoint={endpoint}, model={model}, api_version={api_version}"
@@ -82,7 +78,7 @@ async def get_weather(location: str) -> str:
def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatCompletionClient(azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"))
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert isinstance(client, SupportsChatGetResponse)
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
@@ -93,7 +89,7 @@ def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) ->
def test_init_auto_detects_azure_env(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatCompletionClient()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -115,7 +111,7 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
client = OpenAIChatCompletionClient(credential=lambda: "token")
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -123,34 +119,34 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
def test_init_falls_back_to_generic_azure_deployment_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
client = OpenAIChatCompletionClient()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
def test_init_does_not_fall_back_to_openai_chat_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.setenv("OPENAI_CHAT_MODEL", "test_chat_model")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a deployment name"):
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIChatCompletionClient()
def test_init_does_not_fall_back_to_openai_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.delenv("OPENAI_CHAT_MODEL", raising=False)
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a deployment name"):
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIChatCompletionClient()
@@ -185,7 +185,7 @@ async def test_openai_base64_decoding(openai_unit_test_env: dict[str, str]) -> N
assert abs(expected - actual) < 1e-6
async def test_openai_error_when_no_model_id() -> None:
async def test_openai_error_when_no_model() -> None:
client = OpenAIEmbeddingClient.__new__(OpenAIEmbeddingClient)
client.model = None
client.client = MagicMock()
@@ -19,10 +19,7 @@ pytestmark = pytest.mark.azure
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.openai.azure.com")
or (
os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME", "") == ""
and os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "") == ""
),
or (os.getenv("AZURE_OPENAI_EMBEDDING_MODEL", "") == "" and os.getenv("AZURE_OPENAI_MODEL", "") == ""),
reason="No real Azure OpenAI endpoint or embedding deployment provided; skipping integration tests.",
)
@@ -34,9 +31,7 @@ def _with_azure_openai_debug() -> Any:
try:
return await func(*args, **kwargs)
except Exception as exc:
model = os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") or os.getenv(
"AZURE_OPENAI_DEPLOYMENT_NAME", "<unset>"
)
model = os.getenv("AZURE_OPENAI_EMBEDDING_MODEL") or os.getenv("AZURE_OPENAI_MODEL", "<unset>")
api_version = os.getenv("AZURE_OPENAI_API_VERSION", "<unset>")
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT", "<unset>")
debug_message = f"Azure OpenAI debug: endpoint={endpoint}, model={model}, api_version={api_version}"
@@ -54,7 +49,7 @@ def _with_azure_openai_debug() -> Any:
def _get_azure_embedding_deployment_name() -> str:
return os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") or os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"]
return os.getenv("AZURE_OPENAI_EMBEDDING_MODEL") or os.environ["AZURE_OPENAI_MODEL"]
def _create_azure_embedding_client(
@@ -77,7 +72,7 @@ def _create_azure_embedding_client(
def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None:
client = _create_azure_embedding_client()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -87,7 +82,7 @@ def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) ->
def test_init_auto_detects_azure_embedding_env(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIEmbeddingClient()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -95,34 +90,34 @@ def test_init_auto_detects_azure_embedding_env(azure_openai_unit_test_env: dict[
def test_init_falls_back_to_generic_azure_deployment_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_MODEL", raising=False)
client = OpenAIEmbeddingClient()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
def test_init_does_not_fall_back_to_openai_embedding_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.setenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a deployment name"):
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIEmbeddingClient()
def test_init_does_not_fall_back_to_openai_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_DEPLOYMENT_NAME", raising=False)
monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.delenv("OPENAI_EMBEDDING_MODEL", raising=False)
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a deployment name"):
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIEmbeddingClient()
@@ -156,7 +151,7 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
client = OpenAIEmbeddingClient(credential=lambda: "token")
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -177,7 +172,7 @@ def test_init_with_credential_wraps_async_token_credential(
client = OpenAIEmbeddingClient(credential=credential)
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_MODEL"]
mock_provider.assert_called_once_with(credential, "https://cognitiveservices.azure.com/.default")
@@ -185,7 +180,7 @@ def test_init_with_credential_wraps_async_token_credential(
def test_init_uses_default_azure_api_version(azure_openai_unit_test_env: dict[str, str]) -> None:
client = _create_azure_embedding_client()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_MODEL"]
assert client.api_version == "2024-10-21"
+1 -1
View File
@@ -245,7 +245,7 @@ from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
client = OpenAIChatCompletionClient(
model=os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"],
model=os.environ["AZURE_OPENAI_MODEL"],
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
credential=credential,
middleware=[
@@ -51,7 +51,7 @@ Depending on the selected client, set the appropriate environment variables:
**For Azure OpenAI clients (`azure_openai_responses` and `azure_openai_chat_completion`):**
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint
- `AZURE_OPENAI_DEPLOYMENT_NAME`: The Azure OpenAI deployment used by the sample
- `AZURE_OPENAI_MODEL`: The Azure OpenAI deployment used by the sample
- `AZURE_OPENAI_API_VERSION` (optional): Azure OpenAI API version override
- `AZURE_OPENAI_API_KEY` (optional): Azure OpenAI API key if you are not using `AzureCliCredential`
@@ -66,13 +66,13 @@ Depending on the selected client, set the appropriate environment variables:
**For Anthropic client (`anthropic`):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key
- `ANTHROPIC_CHAT_MODEL_ID`: The Anthropic model ID (for example, `claude-sonnet-4-5`)
- `ANTHROPIC_CHAT_MODEL`: The Anthropic model to use (for example, `claude-sonnet-4-5`)
**For Ollama client (`ollama`):**
- `OLLAMA_HOST`: Ollama server URL (defaults to `http://localhost:11434` if unset)
- `OLLAMA_MODEL_ID`: Ollama model name (for example, `mistral`, `qwen2.5:8b`)
- `OLLAMA_MODEL`: Ollama model name (for example, `mistral`, `qwen2.5:8b`)
**For Bedrock client (`bedrock`):**
- `BEDROCK_CHAT_MODEL_ID`: Bedrock model ID (for example, `anthropic.claude-3-5-sonnet-20240620-v1:0`)
- `BEDROCK_CHAT_MODEL`: Bedrock model ID (for example, `anthropic.claude-3-5-sonnet-20240620-v1:0`)
- `BEDROCK_REGION`: AWS region (defaults to `us-east-1` if unset)
- AWS credentials via standard environment variables (for example, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)

Some files were not shown because too many files have changed in this diff Show More