mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
95550dd0dc
commit
6acab3d1d6
+2
-2
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
```
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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},
|
||||
)
|
||||
+18
-33
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-26
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
```
|
||||
|
||||
|
||||
+8
-13
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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=[])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user