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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

@@ -0,0 +1,201 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Generator
from typing import Any, cast
from unittest.mock import patch
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from pytest import fixture
def _reset_env(monkeypatch, env_names: list[str]) -> None: # type: ignore
for env_name in env_names:
monkeypatch.delenv(env_name, raising=False) # type: ignore
# region Connector Settings fixtures
@fixture
def exclude_list(request: Any) -> list[str]:
"""Fixture that returns a list of environment variables to exclude."""
return request.param if hasattr(request, "param") else []
@fixture
def override_env_param_dict(request: Any) -> dict[str, str]:
"""Fixture that returns a dict of environment variables to override."""
return request.param if hasattr(request, "param") else {}
@fixture()
def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore
"""Fixture to set environment variables for OpenAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
_reset_env(
monkeypatch,
[
"OPENAI_API_KEY",
"OPENAI_ORG_ID",
"OPENAI_MODEL",
"OPENAI_EMBEDDING_MODEL",
"OPENAI_CHAT_COMPLETION_MODEL",
"OPENAI_CHAT_MODEL",
"OPENAI_API_VERSION",
"OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_CHAT_COMPLETION_MODEL",
"AZURE_OPENAI_CHAT_MODEL",
"AZURE_OPENAI_EMBEDDING_MODEL",
"AZURE_OPENAI_MODEL",
"AZURE_OPENAI_API_VERSION",
],
)
env_vars = {
"OPENAI_API_KEY": "test-dummy-key",
"OPENAI_ORG_ID": "test_org_id",
"OPENAI_MODEL": "test_model",
"OPENAI_EMBEDDING_MODEL": "test_embedding_model",
}
env_vars.update(override_env_param_dict) # type: ignore
for key, value in env_vars.items():
if key in exclude_list:
monkeypatch.delenv(key, raising=False) # type: ignore
continue
monkeypatch.setenv(key, value) # type: ignore
return env_vars
@fixture()
def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore
"""Fixture to set environment variables for Azure-backed OpenAI tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
_reset_env(
monkeypatch,
[
"OPENAI_API_KEY",
"OPENAI_ORG_ID",
"OPENAI_MODEL",
"OPENAI_EMBEDDING_MODEL",
"OPENAI_CHAT_COMPLETION_MODEL",
"OPENAI_CHAT_MODEL",
"OPENAI_API_VERSION",
"OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_CHAT_COMPLETION_MODEL",
"AZURE_OPENAI_CHAT_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_COMPLETION_MODEL": "test_chat_deployment",
"AZURE_OPENAI_CHAT_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",
}
env_vars.update(override_env_param_dict) # type: ignore
for key, value in env_vars.items():
if key in exclude_list:
monkeypatch.delenv(key, raising=False) # type: ignore
continue
monkeypatch.setenv(key, value) # type: ignore
return env_vars
# region Observability fixtures
@fixture
def enable_instrumentation(request: Any) -> bool:
"""Fixture that returns a boolean indicating if Otel is enabled."""
return request.param if hasattr(request, "param") else True
@fixture
def enable_sensitive_data(request: Any) -> bool:
"""Fixture that returns a boolean indicating if sensitive data is enabled."""
return request.param if hasattr(request, "param") else True
@fixture
def span_exporter(monkeypatch, enable_instrumentation: bool, enable_sensitive_data: bool) -> Generator[SpanExporter]:
"""Fixture to remove environment variables for ObservabilitySettings."""
env_vars = [
"ENABLE_INSTRUMENTATION",
"ENABLE_SENSITIVE_DATA",
"ENABLE_CONSOLE_EXPORTERS",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
"OTEL_EXPORTER_OTLP_METRICS_HEADERS",
"OTEL_EXPORTER_OTLP_LOGS_HEADERS",
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
"OTEL_RESOURCE_ATTRIBUTES",
]
for key in env_vars:
monkeypatch.delenv(key, raising=False) # type: ignore
monkeypatch.setenv("ENABLE_INSTRUMENTATION", str(enable_instrumentation)) # type: ignore
if not enable_instrumentation:
enable_sensitive_data = False
monkeypatch.setenv("ENABLE_SENSITIVE_DATA", str(enable_sensitive_data)) # type: ignore
import importlib
import agent_framework.observability as observability
from opentelemetry import trace
importlib.reload(observability)
observability_settings = observability.ObservabilitySettings()
if enable_instrumentation or enable_sensitive_data:
from opentelemetry.sdk.trace import TracerProvider
tracer_provider = TracerProvider(resource=cast(Any, observability_settings)._resource)
trace.set_tracer_provider(tracer_provider)
monkeypatch.setattr(observability, "OBSERVABILITY_SETTINGS", observability_settings, raising=False) # type: ignore
with (
patch("agent_framework.observability.OBSERVABILITY_SETTINGS", observability_settings),
patch("agent_framework.observability.configure_otel_providers"),
):
exporter = InMemorySpanExporter()
if enable_instrumentation or enable_sensitive_data:
current_tracer_provider = trace.get_tracer_provider()
if not hasattr(current_tracer_provider, "add_span_processor"):
raise RuntimeError("Tracer provider does not support adding span processors.")
current_tracer_provider.add_span_processor(SimpleSpanProcessor(exporter)) # type: ignore
yield exporter
exporter.clear()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,540 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import os
from functools import wraps
from pathlib import Path
from types import TracebackType
from typing import Any, cast
from unittest.mock import MagicMock, patch
import pytest
from agent_framework import Agent, AgentResponse, ChatResponse, Content, Message, SupportsChatGetResponse, tool
from agent_framework.exceptions import SettingNotFoundError
from azure.core.credentials_async import AsyncTokenCredential
from azure.identity.aio import AzureCliCredential
from openai import AsyncAzureOpenAI
from pydantic import BaseModel
from pytest import param
from agent_framework_openai import OpenAIChatClient
pytestmark = pytest.mark.azure
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skip(
reason="Azure OpenAI integration tests temporarily disabled: crashes the xdist runner in CI.",
)
def _with_azure_openai_debug() -> Any:
def decorator(func: Any) -> Any:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
return await func(*args, **kwargs)
except Exception as exc:
model = os.getenv("AZURE_OPENAI_CHAT_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}"
if hasattr(exc, "add_note"):
cast(Any, exc).add_note(debug_message)
elif exc.args:
exc.args = (f"{exc.args[0]}\n{debug_message}", *exc.args[1:])
else:
exc.args = (debug_message,)
raise
return wrapper
return decorator
class OutputStruct(BaseModel):
"""A structured output for testing purposes."""
location: str
weather: str | None = None
async def create_vector_store(client: OpenAIChatClient) -> tuple[str, Content]:
"""Create a vector store with sample documents for testing."""
file = await client.client.files.create(
file=("todays_weather.txt", b"The weather today is sunny with a high of 75F."),
purpose="assistants",
)
vector_store = await client.client.vector_stores.create(
name="knowledge_base",
expires_after={"anchor": "last_active_at", "days": 1},
)
result = await client.client.vector_stores.files.create_and_poll(
vector_store_id=vector_store.id,
file_id=file.id,
poll_interval_ms=1000,
)
if result.last_error is not None:
raise RuntimeError(f"Vector store file processing failed with status: {result.last_error.message}")
# Wait for the vector store index to be fully searchable.
# create_and_poll confirms file processing, but the search index is eventually consistent.
for _ in range(10):
vs = await client.client.vector_stores.retrieve(vector_store.id)
if vs.file_counts.completed >= 1 and vs.file_counts.in_progress == 0:
break
await asyncio.sleep(1)
await asyncio.sleep(2)
return file.id, Content.from_hosted_vector_store(vector_store_id=vector_store.id)
async def delete_vector_store(client: OpenAIChatClient, file_id: str, vector_store_id: str) -> None:
"""Delete the vector store after tests."""
await client.client.vector_stores.delete(vector_store_id=vector_store_id)
await client.client.files.delete(file_id=file_id)
@tool(approval_mode="never_require")
async def get_weather(location: str) -> str:
"""Get the current weather in a given location."""
return f"The current weather in {location} is sunny."
def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatClient(credential=cast(Any, AzureCliCredential()))
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"
assert client.azure_endpoint is not None
assert client.azure_endpoint.startswith(azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"])
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_CHAT_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
def test_openai_api_key_wins_over_azure_env(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
client = OpenAIChatClient()
assert client.model == "gpt-5"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint is None
def test_api_version_alone_does_not_override_openai_api_key(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
client = OpenAIChatClient(api_version="2024-10-21")
assert client.model == "gpt-5"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint is None
def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
client = OpenAIChatClient(credential=lambda: "token")
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"]
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_MODEL", raising=False)
client = OpenAIChatClient()
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_CHAT_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.setenv("OPENAI_CHAT_MODEL", "test_responses_model")
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_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 model"):
OpenAIChatClient()
def test_init_with_credential_wraps_async_token_credential(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
class TestAsyncTokenCredential(AsyncTokenCredential):
async def get_token(self, *scopes: str, **kwargs: object):
raise NotImplementedError
async def close(self) -> None:
pass
async def __aexit__(
self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: TracebackType | None = None,
) -> None:
pass
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
credential = TestAsyncTokenCredential()
token_provider = MagicMock()
with patch("azure.identity.aio.get_bearer_token_provider", return_value=token_provider) as mock_provider:
client = OpenAIChatClient(credential=cast(Any, credential))
assert isinstance(client.client, AsyncAzureOpenAI)
mock_provider.assert_called_once_with(credential, "https://cognitiveservices.azure.com/.default")
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_VERSION"]], indirect=True)
def test_init_uses_default_azure_api_version(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatClient(credential=cast(Any, AzureCliCredential()))
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert client.api_version is not None
def test_openai_base_url_wins_over_azure_aliases(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
monkeypatch.setenv("OPENAI_BASE_URL", "https://custom-openai-endpoint.com/v1")
client = OpenAIChatClient()
assert client.model == "gpt-5"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint is None
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@pytest.mark.parametrize(
"option_name,option_value,needs_validation",
[
param("max_tokens", 500, False, id="max_tokens"),
param("seed", 123, False, id="seed"),
param("user", "test-user-id", False, id="user"),
param("metadata", {"test_key": "test_value"}, False, id="metadata"),
param("frequency_penalty", 0.5, False, id="frequency_penalty"),
param("presence_penalty", 0.3, False, id="presence_penalty"),
param("stop", ["END"], False, id="stop"),
param("allow_multiple_tool_calls", True, False, id="allow_multiple_tool_calls"),
param("tool_choice", "none", True, id="tool_choice_none"),
param("safety_identifier", "user-hash-abc123", False, id="safety_identifier"),
param("truncation", "auto", False, id="truncation"),
param("prompt_cache_key", "test-cache-key", False, id="prompt_cache_key"),
param("max_tool_calls", 3, False, id="max_tool_calls"),
param("tools", [get_weather], True, id="tools_function"),
param("tool_choice", "auto", True, id="tool_choice_auto"),
param(
"tool_choice",
{"mode": "required", "required_function_name": "get_weather"},
True,
id="tool_choice_required",
),
param("response_format", OutputStruct, True, id="response_format_pydantic"),
param(
"response_format",
{
"type": "json_schema",
"json_schema": {
"name": "WeatherDigest",
"strict": True,
"schema": {
"title": "WeatherDigest",
"type": "object",
"properties": {
"location": {"type": "string"},
"conditions": {"type": "string"},
"temperature_c": {"type": "number"},
"advisory": {"type": "string"},
},
"required": ["location", "conditions", "temperature_c", "advisory"],
"additionalProperties": False,
},
},
},
True,
id="response_format_runtime_json_schema",
),
],
)
@_with_azure_openai_debug()
async def test_integration_options(
option_name: str,
option_value: Any,
needs_validation: bool,
) -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=cast(Any, credential))
client.function_invocation_configuration["max_iterations"] = 2
for streaming in [False, True]:
if option_name in {"tools", "tool_choice"}:
messages = [Message(role="user", contents=["What is the weather in Seattle?"])]
elif option_name == "response_format":
messages = [
Message(role="user", contents=["The weather in Seattle is sunny"]),
Message(role="user", contents=["What is the weather in Seattle?"]),
]
else:
messages = [Message(role="user", contents=["Say 'Hello World' briefly."])]
options: dict[str, Any] = {option_name: option_value}
if option_name == "tool_choice":
options["tools"] = [get_weather]
if streaming:
response = (
await cast(Any, client)
.get_response(
messages=messages,
stream=True,
options=options,
)
.get_final_response()
)
else:
response = await cast(Any, client).get_response(messages=messages, options=options)
assert isinstance(response, ChatResponse)
assert response.text is not None
assert len(response.text) > 0
if needs_validation:
if option_name in {"tools", "tool_choice"}:
text = response.text.lower()
assert "sunny" in text or "seattle" in text
elif option_name == "response_format":
if option_value == OutputStruct:
assert response.value is not None
assert isinstance(response.value, OutputStruct)
assert "seattle" in response.value.location.lower()
else:
assert response.value is not None
assert isinstance(response.value, dict)
assert "location" in response.value
assert "seattle" in response.value["location"].lower()
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_integration_web_search() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=cast(Any, credential))
response = await client.get_response(
messages=[
Message(
role="user",
contents=["What is the current weather? Do not ask for my current location."],
)
],
options={
"tools": [OpenAIChatClient.get_web_search_tool(user_location={"country": "US", "city": "Seattle"})],
},
stream=True,
).get_final_response()
assert isinstance(response, ChatResponse)
assert response.text is not None
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_integration_client_file_search() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=cast(Any, credential))
file_id, vector_store = await create_vector_store(client)
vector_store_id = vector_store.vector_store_id
assert vector_store_id is not None
try:
response = await cast(Any, client).get_response(
messages=[
Message(role="user", contents=["What is the weather today? Do a file search to find the answer."])
],
options={
"tools": [OpenAIChatClient.get_file_search_tool(vector_store_ids=[vector_store_id])],
"tool_choice": "auto",
},
)
assert "sunny" in response.text.lower()
assert "75" in response.text
finally:
await delete_vector_store(client, file_id, vector_store_id)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_integration_client_file_search_streaming() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=cast(Any, credential))
file_id, vector_store = await create_vector_store(client)
vector_store_id = vector_store.vector_store_id
assert vector_store_id is not None
try:
response_stream = cast(Any, client).get_response(
messages=[
Message(role="user", contents=["What is the weather today? Do a file search to find the answer."])
],
stream=True,
options={
"tools": [OpenAIChatClient.get_file_search_tool(vector_store_ids=[vector_store_id])],
"tool_choice": "auto",
},
)
full_response = await response_stream.get_final_response()
assert "sunny" in full_response.text.lower()
assert "75" in full_response.text
finally:
await delete_vector_store(client, file_id, vector_store_id)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_integration_client_agent_hosted_mcp_tool() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=cast(Any, credential))
response = await client.get_response(
messages=[Message(role="user", contents=["How to create an Azure storage account using az cli?"])],
options={
"max_tokens": 5000,
"tools": OpenAIChatClient.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
),
},
)
assert isinstance(response, ChatResponse)
if not response.text:
pytest.skip("MCP server returned empty response - service-side issue")
assert any(term in response.text.lower() for term in ["azure", "storage", "account", "cli"])
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_integration_client_agent_hosted_code_interpreter_tool() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=cast(Any, credential))
response = await client.get_response(
messages=[Message(role="user", contents=["Calculate the sum of numbers from 1 to 10 using Python code."])],
options={"tools": [OpenAIChatClient.get_code_interpreter_tool()]},
)
contains_relevant_content = any(
term in response.text.lower() for term in ["55", "sum", "code", "python", "calculate", "10"]
)
assert contains_relevant_content or len(response.text.strip()) > 10
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_integration_client_agent_existing_session() -> None:
async with AzureCliCredential() as credential:
preserved_session = None
async with Agent(
client=OpenAIChatClient(credential=cast(Any, credential)),
instructions="You are a helpful assistant with good memory.",
) as first_agent:
session = first_agent.create_session()
first_response = await first_agent.run(
"My hobby is photography. Remember this.",
session=session,
options={"store": True},
)
assert isinstance(first_response, AgentResponse)
preserved_session = session
if preserved_session:
async with Agent(
client=OpenAIChatClient(credential=cast(Any, credential)),
instructions="You are a helpful assistant with good memory.",
) as second_agent:
second_response = await second_agent.run(
"What is my hobby?", session=preserved_session, options={"store": True}
)
assert isinstance(second_response, AgentResponse)
assert second_response.text is not None
assert "photography" in second_response.text.lower()
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
@pytest.mark.skip(reason="Azure OpenAI is flaky when handling image content as function result. Needs investigation.")
async def test_azure_openai_chat_client_tool_rich_content_image() -> None:
image_path = Path(__file__).parent.parent / "assets" / "sample_image.jpg"
image_bytes = image_path.read_bytes()
@tool(approval_mode="never_require")
def get_test_image() -> Content:
"""Return a test image for analysis."""
return Content.from_data(data=image_bytes, media_type="image/jpeg")
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=cast(Any, credential))
client.function_invocation_configuration["max_iterations"] = 2
response = await client.get_response(
messages=[Message(role="user", contents=["Call the get_test_image tool and describe what you see."])],
stream=True,
options={"tools": [get_test_image], "tool_choice": "auto"},
).get_final_response()
assert isinstance(response, ChatResponse)
assert response.text is not None
assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,418 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import os
from functools import wraps
from types import TracebackType
from typing import Any, cast
from unittest.mock import MagicMock, patch
import pytest
from agent_framework import (
Agent,
AgentResponse,
AgentResponseUpdate,
ChatResponse,
ChatResponseUpdate,
Message,
SupportsChatGetResponse,
tool,
)
from agent_framework.exceptions import SettingNotFoundError
from azure.core.credentials_async import AsyncTokenCredential
from azure.identity.aio import AzureCliCredential
from openai import AsyncAzureOpenAI
from agent_framework_openai import OpenAIChatCompletionClient
pytestmark = pytest.mark.azure
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skip(
reason="Azure OpenAI integration tests temporarily disabled: crashes the xdist runner in CI.",
)
def _with_azure_openai_debug() -> Any:
def decorator(func: Any) -> Any:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
return await func(*args, **kwargs)
except Exception as exc:
model = os.getenv("AZURE_OPENAI_CHAT_COMPLETION_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}"
if hasattr(exc, "add_note"):
cast(Any, exc).add_note(debug_message)
elif exc.args:
exc.args = (f"{exc.args[0]}\n{debug_message}", *exc.args[1:])
else:
exc.args = (debug_message,)
raise
return wrapper
return decorator
@tool(approval_mode="never_require")
def get_story_text() -> str:
"""Returns a story about Emily and David."""
return (
"Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
"of climate change."
)
@tool(approval_mode="never_require")
async def get_weather(location: str) -> str:
"""Get the current weather in a given location."""
return f"The current weather in {location} is sunny, 72F."
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_COMPLETION_MODEL"]
assert isinstance(client, SupportsChatGetResponse)
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"]
assert client.api_version == azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"]
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_COMPLETION_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
def test_openai_api_key_wins_over_azure_env(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
client = OpenAIChatCompletionClient()
assert client.model == "gpt-5"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint is None
def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
client = OpenAIChatCompletionClient(credential=lambda: "token")
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_COMPLETION_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
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_COMPLETION_MODEL", raising=False)
client = OpenAIChatCompletionClient()
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_COMPLETION_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.setenv("OPENAI_CHAT_COMPLETION_MODEL", "test_chat_model")
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_COMPLETION_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.delenv("OPENAI_CHAT_COMPLETION_MODEL", raising=False)
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIChatCompletionClient()
def test_init_with_credential_wraps_async_token_credential(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_API_KEY", raising=False)
class TestAsyncTokenCredential(AsyncTokenCredential):
async def get_token(self, *scopes: str, **kwargs: object):
raise NotImplementedError
async def close(self) -> None:
pass
async def __aexit__(
self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: TracebackType | None = None,
) -> None:
pass
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
credential = TestAsyncTokenCredential()
token_provider = MagicMock()
with patch("azure.identity.aio.get_bearer_token_provider", return_value=token_provider) as mock_provider:
client = OpenAIChatCompletionClient(credential=cast(Any, credential))
assert isinstance(client.client, AsyncAzureOpenAI)
mock_provider.assert_called_once_with(credential, "https://cognitiveservices.azure.com/.default")
def test_openai_base_url_wins_over_azure_aliases(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
monkeypatch.setenv("OPENAI_BASE_URL", "https://custom-openai-endpoint.com/v1")
client = OpenAIChatCompletionClient()
assert client.model == "gpt-5"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint is None
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_chat_completion_client_response() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatCompletionClient(credential=cast(Any, credential))
assert isinstance(client, SupportsChatGetResponse)
messages = [
Message(
role="user",
contents=[
(
"Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
"of climate change."
)
],
),
Message(role="user", contents=["who are Emily and David?"]),
]
response = await client.get_response(messages=messages)
assert response is not None
assert isinstance(response, ChatResponse)
assert any(
word in response.text.lower() for word in ["scientists", "research", "antarctica", "glaciology", "climate"]
)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_chat_completion_client_response_tools() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatCompletionClient(credential=cast(Any, credential))
response = await client.get_response(
messages=[Message(role="user", contents=["who are Emily and David?"])],
options={"tools": [get_story_text], "tool_choice": "auto"},
)
assert response is not None
assert isinstance(response, ChatResponse)
assert "Emily" in response.text or "David" in response.text
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_chat_completion_client_streaming() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatCompletionClient(credential=cast(Any, credential))
response = client.get_response(
messages=[
Message(
role="user",
contents=[
(
"Emily and David, two passionate scientists, met during a research expedition to "
"Antarctica. Bonded by their love for the natural world and shared curiosity, they "
"uncovered a groundbreaking phenomenon in glaciology that could potentially reshape our "
"understanding of climate change."
)
],
),
Message(role="user", contents=["who are Emily and David?"]),
],
stream=True,
)
full_message = ""
async for chunk in response:
assert isinstance(chunk, ChatResponseUpdate)
assert chunk.message_id is not None
assert chunk.response_id is not None
for content in chunk.contents:
if content.type == "text" and content.text:
full_message += content.text
assert "Emily" in full_message or "David" in full_message
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_chat_completion_client_streaming_tools() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatCompletionClient(credential=cast(Any, credential))
response = client.get_response(
messages=[Message(role="user", contents=["who are Emily and David?"])],
stream=True,
options={"tools": [get_story_text], "tool_choice": "auto"},
)
full_message = ""
async for chunk in response:
assert isinstance(chunk, ChatResponseUpdate)
for content in chunk.contents:
if content.type == "text" and content.text:
full_message += content.text
assert "Emily" in full_message or "David" in full_message
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_chat_completion_client_agent_basic_run() -> None:
async with (
AzureCliCredential() as credential,
Agent(
client=OpenAIChatCompletionClient(credential=cast(Any, credential)),
) as agent,
):
response = await agent.run("Please respond with exactly: 'This is a response test.'")
assert isinstance(response, AgentResponse)
assert response.text is not None
assert "response test" in response.text.lower()
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_chat_completion_client_agent_basic_run_streaming() -> None:
async with (
AzureCliCredential() as credential,
Agent(client=OpenAIChatCompletionClient(credential=cast(Any, credential))) as agent,
):
full_text = ""
async for chunk in agent.run("Please respond with exactly: 'This is a streaming response test.'", stream=True):
assert isinstance(chunk, AgentResponseUpdate)
if chunk.text:
full_text += chunk.text
assert "streaming response test" in full_text.lower()
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_chat_completion_client_agent_session_persistence() -> None:
async with (
AzureCliCredential() as credential,
Agent(
client=OpenAIChatCompletionClient(credential=cast(Any, credential)),
instructions="You are a helpful assistant with good memory.",
) as agent,
):
session = agent.create_session()
response1 = await agent.run("My name is Alice. Remember this.", session=session)
response2 = await agent.run("What is my name?", session=session)
assert isinstance(response1, AgentResponse)
assert isinstance(response2, AgentResponse)
assert response2.text is not None
assert "alice" in response2.text.lower()
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_chat_completion_client_agent_existing_session() -> None:
async with AzureCliCredential() as credential:
preserved_session = None
async with Agent(
client=OpenAIChatCompletionClient(credential=cast(Any, credential)),
instructions="You are a helpful assistant with good memory.",
) as first_agent:
session = first_agent.create_session()
first_response = await first_agent.run("My name is Alice. Remember this.", session=session)
assert isinstance(first_response, AgentResponse)
preserved_session = session
if preserved_session:
async with Agent(
client=OpenAIChatCompletionClient(credential=cast(Any, credential)),
instructions="You are a helpful assistant with good memory.",
) as second_agent:
second_response = await second_agent.run("What is my name?", session=preserved_session)
assert isinstance(second_response, AgentResponse)
assert second_response.text is not None
assert "alice" in second_response.text.lower()
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_chat_completion_client_agent_level_tool_persistence() -> None:
async with (
AzureCliCredential() as credential,
Agent(
client=OpenAIChatCompletionClient(credential=cast(Any, credential)),
instructions="You are a helpful assistant that uses available tools.",
tools=[get_weather],
) as agent,
):
first_response = await agent.run("What's the weather like in Chicago?")
second_response = await agent.run("What's the weather in Miami?")
assert isinstance(first_response, AgentResponse)
assert isinstance(second_response, AgentResponse)
assert first_response.text is not None
assert second_response.text is not None
assert any(term in first_response.text.lower() for term in ["chicago", "sunny", "72"])
assert any(term in second_response.text.lower() for term in ["miami", "sunny", "72"])
@@ -0,0 +1,429 @@
# Copyright (c) Microsoft. All rights reserved.
from copy import deepcopy
from datetime import datetime, timezone
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import ChatResponseUpdate, Message
from agent_framework.exceptions import ChatClientException
from openai import AsyncStream
from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions
from openai.types.chat import ChatCompletion, ChatCompletionChunk
from openai.types.chat.chat_completion import Choice
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDelta
from openai.types.chat.chat_completion_message import ChatCompletionMessage
from pydantic import BaseModel
from agent_framework_openai import OpenAIChatCompletionClient
async def mock_async_process_chat_stream_response(_):
mock_content = MagicMock(spec=ChatResponseUpdate)
yield mock_content, None
@pytest.fixture(scope="function")
def chat_history() -> list[Message]:
return []
@pytest.fixture
def mock_chat_completion_response() -> ChatCompletion:
return ChatCompletion(
id="test_id",
choices=[
Choice(index=0, message=ChatCompletionMessage(content="test", role="assistant"), finish_reason="stop")
],
created=0,
model="test",
object="chat.completion",
)
@pytest.fixture
def mock_streaming_chat_completion_response() -> AsyncStream[ChatCompletionChunk]:
content = ChatCompletionChunk(
id="test_id",
choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")],
created=0,
model="test",
object="chat.completion.chunk",
)
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content]
return stream
# region Chat Message Content
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc(
mock_create: AsyncMock,
chat_history: list[Message],
mock_chat_completion_response: ChatCompletion,
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(Message(role="user", contents=["hello world"]))
openai_chat_completion = OpenAIChatCompletionClient()
await openai_chat_completion.get_response(messages=chat_history)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_MODEL"],
stream=False,
messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_chat_options(
mock_create: AsyncMock,
chat_history: list[Message],
mock_chat_completion_response: ChatCompletion,
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(Message(role="user", contents=["hello world"]))
openai_chat_completion = OpenAIChatCompletionClient()
await openai_chat_completion.get_response(
messages=chat_history,
)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_MODEL"],
stream=False,
messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_no_fcc_in_response(
mock_create: AsyncMock,
chat_history: list[Message],
mock_chat_completion_response: ChatCompletion,
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(Message(role="user", contents=["hello world"]))
orig_chat_history = deepcopy(chat_history)
openai_chat_completion = OpenAIChatCompletionClient()
await openai_chat_completion.get_response(
messages=chat_history,
)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_MODEL"],
stream=False,
messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_structured_output_no_fcc(
mock_create: AsyncMock,
chat_history: list[Message],
mock_chat_completion_response: ChatCompletion,
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(Message(role="user", contents=["hello world"]))
# Define a mock response format
class Test(BaseModel):
name: str
openai_chat_completion = OpenAIChatCompletionClient()
await openai_chat_completion.get_response(
messages=chat_history,
options={"response_format": Test},
)
mock_create.assert_awaited_once()
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_scmc_chat_options(
mock_create: AsyncMock,
chat_history: list[Message],
mock_streaming_chat_completion_response: AsyncStream[ChatCompletionChunk],
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_streaming_chat_completion_response
chat_history.append(Message(role="user", contents=["hello world"]))
openai_chat_completion = OpenAIChatCompletionClient()
async for msg in openai_chat_completion.get_response(
stream=True,
messages=chat_history,
):
assert isinstance(msg, ChatResponseUpdate)
assert msg.message_id is not None
assert msg.response_id is not None
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_MODEL"],
stream=True,
stream_options={"include_usage": True},
messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock, side_effect=Exception)
async def test_cmc_general_exception(
mock_create: AsyncMock,
chat_history: list[Message],
mock_chat_completion_response: ChatCompletion,
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(Message(role="user", contents=["hello world"]))
openai_chat_completion = OpenAIChatCompletionClient()
with pytest.raises(ChatClientException):
await openai_chat_completion.get_response(
messages=chat_history,
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_additional_properties(
mock_create: AsyncMock,
chat_history: list[Message],
mock_chat_completion_response: ChatCompletion,
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(Message(role="user", contents=["hello world"]))
openai_chat_completion = OpenAIChatCompletionClient()
await cast(Any, openai_chat_completion).get_response(
messages=chat_history,
options={"reasoning_effort": "low"},
)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_MODEL"],
stream=False,
messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore
reasoning_effort="low",
)
# region Streaming
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_get_streaming(
mock_create: AsyncMock,
chat_history: list[Message],
openai_unit_test_env: dict[str, str],
):
content1 = ChatCompletionChunk(
id="test_id",
choices=[],
created=0,
model="test",
object="chat.completion.chunk",
)
content2 = ChatCompletionChunk(
id="test_id",
choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")],
created=0,
model="test",
object="chat.completion.chunk",
)
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content1, content2]
mock_create.return_value = stream
chat_history.append(Message(role="user", contents=["hello world"]))
orig_chat_history = deepcopy(chat_history)
openai_chat_completion = OpenAIChatCompletionClient()
async for msg in openai_chat_completion.get_response(
stream=True,
messages=chat_history,
):
assert isinstance(msg, ChatResponseUpdate)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_MODEL"],
stream=True,
stream_options={"include_usage": True},
messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_get_streaming_singular(
mock_create: AsyncMock,
chat_history: list[Message],
openai_unit_test_env: dict[str, str],
):
content1 = ChatCompletionChunk(
id="test_id",
choices=[],
created=0,
model="test",
object="chat.completion.chunk",
)
content2 = ChatCompletionChunk(
id="test_id",
choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")],
created=0,
model="test",
object="chat.completion.chunk",
)
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content1, content2]
mock_create.return_value = stream
chat_history.append(Message(role="user", contents=["hello world"]))
orig_chat_history = deepcopy(chat_history)
openai_chat_completion = OpenAIChatCompletionClient()
async for msg in openai_chat_completion.get_response(
stream=True,
messages=chat_history,
):
assert isinstance(msg, ChatResponseUpdate)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_MODEL"],
stream=True,
stream_options={"include_usage": True},
messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_get_streaming_structured_output_no_fcc(
mock_create: AsyncMock,
chat_history: list[Message],
openai_unit_test_env: dict[str, str],
):
content1 = ChatCompletionChunk(
id="test_id",
choices=[],
created=0,
model="test",
object="chat.completion.chunk",
)
content2 = ChatCompletionChunk(
id="test_id",
choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")],
created=0,
model="test",
object="chat.completion.chunk",
)
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content1, content2]
mock_create.return_value = stream
chat_history.append(Message(role="user", contents=["hello world"]))
# Define a mock response format
class Test(BaseModel):
name: str
openai_chat_completion = OpenAIChatCompletionClient()
async for msg in openai_chat_completion.get_response(
stream=True,
messages=chat_history,
options={"response_format": Test},
):
assert isinstance(msg, ChatResponseUpdate)
mock_create.assert_awaited_once()
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_get_streaming_no_fcc_in_response(
mock_create: AsyncMock,
chat_history: list[Message],
mock_streaming_chat_completion_response: ChatCompletion,
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_streaming_chat_completion_response
chat_history.append(Message(role="user", contents=["hello world"]))
orig_chat_history = deepcopy(chat_history)
openai_chat_completion = OpenAIChatCompletionClient()
[
msg
async for msg in openai_chat_completion.get_response(
stream=True,
messages=chat_history,
)
]
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_MODEL"],
stream=True,
stream_options={"include_usage": True},
messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore
)
# region UTC Timestamp Tests
def test_chat_response_created_at_uses_utc(openai_unit_test_env: dict[str, str]):
"""Test that ChatResponse.created_at uses UTC timestamp, not local time.
This is a regression test for the issue where created_at was using local time
but labeling it as UTC (with 'Z' suffix).
"""
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
# This ensures we test that the timestamp is actually converted to UTC
utc_timestamp = 1733011890
mock_response = ChatCompletion(
id="test_id",
choices=[
Choice(index=0, message=ChatCompletionMessage(content="test", role="assistant"), finish_reason="stop")
],
created=utc_timestamp,
model="test",
object="chat.completion",
)
client = OpenAIChatCompletionClient()
response = client._parse_response_from_openai(mock_response, {})
# Verify that created_at is correctly formatted as UTC
assert response.created_at is not None
assert response.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
# Parse the timestamp and verify it matches UTC time
expected_utc_time = datetime.fromtimestamp(utc_timestamp, tz=timezone.utc)
expected_formatted = expected_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
assert response.created_at == expected_formatted, (
f"Expected UTC timestamp {expected_formatted}, got {response.created_at}"
)
def test_chat_response_update_created_at_uses_utc(openai_unit_test_env: dict[str, str]):
"""Test that ChatResponseUpdate.created_at uses UTC timestamp, not local time.
This is a regression test for the issue where created_at was using local time
but labeling it as UTC (with 'Z' suffix).
"""
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
utc_timestamp = 1733011890
mock_chunk = ChatCompletionChunk(
id="test_id",
choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")],
created=utc_timestamp,
model="test",
object="chat.completion.chunk",
)
client = OpenAIChatCompletionClient()
response_update = client._parse_response_update_from_openai(mock_chunk)
# Verify that created_at is correctly formatted as UTC
assert response_update.created_at is not None
assert response_update.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
# Parse the timestamp and verify it matches UTC time
expected_utc_time = datetime.fromtimestamp(utc_timestamp, tz=timezone.utc)
expected_formatted = expected_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
assert response_update.created_at == expected_formatted, (
f"Expected UTC timestamp {expected_formatted}, got {response_update.created_at}"
)
@@ -0,0 +1,266 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import inspect
import os
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock
import pytest
from agent_framework import SupportsGetEmbeddings
from agent_framework.exceptions import SettingNotFoundError
from openai.types import CreateEmbeddingResponse
from openai.types import Embedding as OpenAIEmbedding
from openai.types.create_embedding_response import Usage
from agent_framework_openai import (
OpenAIEmbeddingClient,
OpenAIEmbeddingOptions,
)
from agent_framework_openai._embedding_client import RawOpenAIEmbeddingClient
def _make_openai_response(
embeddings: list[list[float]],
model: str = "text-embedding-3-small",
prompt_tokens: int = 5,
total_tokens: int = 5,
) -> CreateEmbeddingResponse:
"""Helper to create a mock OpenAI embeddings response."""
data = [OpenAIEmbedding(embedding=emb, index=i, object="embedding") for i, emb in enumerate(embeddings)]
return CreateEmbeddingResponse(
data=data,
model=model,
object="list",
usage=Usage(prompt_tokens=prompt_tokens, total_tokens=total_tokens),
)
# --- OpenAI unit tests ---
def test_openai_construction_with_explicit_params() -> None:
client = OpenAIEmbeddingClient(
model="text-embedding-3-small",
api_key="test-key",
)
assert client.model == "text-embedding-3-small"
assert isinstance(client, SupportsGetEmbeddings)
def test_raw_openai_embedding_client_init_uses_explicit_parameters() -> None:
signature = inspect.signature(RawOpenAIEmbeddingClient.__init__)
assert "additional_properties" in signature.parameters
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
def test_openai_construction_from_env(openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIEmbeddingClient()
assert client.model == openai_unit_test_env["OPENAI_EMBEDDING_MODEL"]
def test_with_callable_api_key() -> None:
"""Test OpenAIEmbeddingClient initialization with callable API key."""
async def get_api_key() -> str:
return "test-api-key-123"
client = OpenAIEmbeddingClient(model="text-embedding-3-small", api_key=get_api_key)
assert client.model == "text-embedding-3-small"
assert client.client is not None
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_openai_construction_without_openai_or_azure_config_raises_clear_error(
openai_unit_test_env: dict[str, str],
) -> None:
with pytest.raises(SettingNotFoundError):
OpenAIEmbeddingClient(model="text-embedding-3-small")
@pytest.mark.parametrize("exclude_list", [["OPENAI_EMBEDDING_MODEL"]], indirect=True)
def test_openai_construction_falls_back_to_openai_model(openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIEmbeddingClient()
assert client.model == openai_unit_test_env["OPENAI_MODEL"]
async def test_openai_get_embeddings(openai_unit_test_env: dict[str, str]) -> None:
mock_response = _make_openai_response(
embeddings=[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]],
)
client = OpenAIEmbeddingClient()
client.client = MagicMock()
client.client.embeddings = MagicMock()
client.client.embeddings.create = AsyncMock(return_value=mock_response)
result = await client.get_embeddings(["hello", "world"])
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 == "text-embedding-3-small"
assert result[0].dimensions == 3
async def test_openai_get_embeddings_usage(openai_unit_test_env: dict[str, str]) -> None:
mock_response = _make_openai_response(
embeddings=[[0.1]],
prompt_tokens=10,
total_tokens=10,
)
client = OpenAIEmbeddingClient()
client.client = MagicMock()
client.client.embeddings = MagicMock()
client.client.embeddings.create = AsyncMock(return_value=mock_response)
result = await client.get_embeddings(["test"])
assert result.usage is not None
assert result.usage["input_token_count"] == 10
assert result.usage["total_token_count"] == 10
async def test_openai_options_passthrough_dimensions(openai_unit_test_env: dict[str, str]) -> None:
mock_response = _make_openai_response(embeddings=[[0.1]])
client = OpenAIEmbeddingClient()
client.client = MagicMock()
client.client.embeddings = MagicMock()
client.client.embeddings.create = AsyncMock(return_value=mock_response)
options: OpenAIEmbeddingOptions = {"dimensions": 256}
result = await client.get_embeddings(["test"], options=options)
call_kwargs = client.client.embeddings.create.call_args[1]
assert call_kwargs["dimensions"] == 256
assert result.options is options
async def test_openai_options_passthrough_encoding_format(openai_unit_test_env: dict[str, str]) -> None:
mock_response = _make_openai_response(embeddings=[[0.1]])
client = OpenAIEmbeddingClient()
client.client = MagicMock()
client.client.embeddings = MagicMock()
client.client.embeddings.create = AsyncMock(return_value=mock_response)
options: OpenAIEmbeddingOptions = {"encoding_format": "base64"}
await client.get_embeddings(["test"], options=options)
call_kwargs = client.client.embeddings.create.call_args[1]
assert call_kwargs["encoding_format"] == "base64"
async def test_openai_base64_decoding(openai_unit_test_env: dict[str, str]) -> None:
import base64
import struct
# Encode [0.1, 0.2, 0.3] as base64 little-endian floats
raw_floats = [0.1, 0.2, 0.3]
b64_str = base64.b64encode(struct.pack(f"<{len(raw_floats)}f", *raw_floats)).decode()
# Mock the embedding item to return a base64 string (as the API does with encoding_format=base64)
mock_item = MagicMock()
mock_item.embedding = b64_str
mock_item.index = 0
mock_response = MagicMock()
mock_response.data = [mock_item]
mock_response.model = "text-embedding-3-small"
mock_response.usage = MagicMock(prompt_tokens=3, total_tokens=3)
client = OpenAIEmbeddingClient()
client.client = MagicMock()
client.client.embeddings = MagicMock()
client.client.embeddings.create = AsyncMock(return_value=mock_response)
options: OpenAIEmbeddingOptions = {"encoding_format": "base64"}
result = await client.get_embeddings(["test"], options=options)
assert len(result) == 1
assert len(result[0].vector) == 3
assert result[0].dimensions == 3
for expected, actual in zip(raw_floats, result[0].vector):
assert abs(expected - actual) < 1e-6
async def test_openai_error_when_no_model() -> None:
client = cast(Any, object.__new__(OpenAIEmbeddingClient))
client.model = None
client.client = MagicMock()
client.additional_properties = {}
client.otel_provider_name = "openai"
with pytest.raises(ValueError, match="model is required"):
await client.get_embeddings(["test"])
async def test_openai_empty_values_returns_empty(openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIEmbeddingClient()
client.client = MagicMock()
client.client.embeddings = MagicMock()
client.client.embeddings.create = AsyncMock()
result = await client.get_embeddings([])
assert len(result) == 0
assert result.usage is None
client.client.embeddings.create.assert_not_called()
# --- Integration tests ---
skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"),
reason="No real OPENAI_API_KEY provided; skipping integration tests.",
)
@skip_if_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_openai_get_embeddings() -> None:
"""End-to-end test of OpenAI embedding generation."""
client = OpenAIEmbeddingClient(model="text-embedding-3-small")
result = await client.get_embeddings(["hello world"])
assert len(result) == 1
assert isinstance(result[0].vector, list)
assert len(result[0].vector) > 0
assert all(isinstance(v, float) for v in result[0].vector)
assert result[0].model is not None
assert result.usage is not None
input_token_count = result.usage["input_token_count"]
assert input_token_count is not None
assert input_token_count > 0
@skip_if_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_openai_get_embeddings_multiple() -> None:
"""Test embedding generation for multiple inputs."""
client = OpenAIEmbeddingClient(model="text-embedding-3-small")
result = await client.get_embeddings(["hello", "world", "test"])
assert len(result) == 3
dims = [len(e.vector) for e in result]
assert all(d == dims[0] for d in dims)
@skip_if_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_openai_get_embeddings_with_dimensions() -> None:
"""Test embedding generation with custom dimensions."""
client = OpenAIEmbeddingClient(model="text-embedding-3-small")
options: OpenAIEmbeddingOptions = {"dimensions": 256}
result = await client.get_embeddings(["hello world"], options=options)
assert len(result) == 1
assert len(result[0].vector) == 256
@@ -0,0 +1,329 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import os
from functools import wraps
from types import TracebackType
from typing import Any, cast
from unittest.mock import MagicMock, patch
import pytest
from agent_framework.exceptions import SettingNotFoundError
from azure.core.credentials_async import AsyncTokenCredential
from azure.identity.aio import AzureCliCredential
from openai import AsyncAzureOpenAI, AsyncOpenAI
from agent_framework_openai import OpenAIEmbeddingClient, OpenAIEmbeddingOptions
pytestmark = pytest.mark.azure
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skip(
reason="Azure OpenAI integration tests temporarily disabled: crashes the xdist runner in CI.",
)
def _with_azure_openai_debug() -> Any:
def decorator(func: Any) -> Any:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
return await func(*args, **kwargs)
except Exception as exc:
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}"
if hasattr(exc, "add_note"):
cast(Any, exc).add_note(debug_message)
elif exc.args:
exc.args = (f"{exc.args[0]}\n{debug_message}", *exc.args[1:])
else:
exc.args = (debug_message,)
raise
return wrapper
return decorator
def _get_azure_embedding_deployment_name() -> str:
return os.getenv("AZURE_OPENAI_EMBEDDING_MODEL") or os.environ["AZURE_OPENAI_MODEL"]
def _create_azure_embedding_client(
*,
api_key: str | None = None,
credential: AsyncTokenCredential | None = None,
) -> OpenAIEmbeddingClient:
resolved_api_key = (
api_key if api_key is not None else None if credential is not None else os.environ["AZURE_OPENAI_API_KEY"]
)
return OpenAIEmbeddingClient(
model=_get_azure_embedding_deployment_name(),
api_key=resolved_api_key,
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
credential=cast(Any, credential),
)
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_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"]
assert client.api_version == azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"]
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_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
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_MODEL", raising=False)
client = OpenAIEmbeddingClient()
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_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 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_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 model"):
OpenAIEmbeddingClient()
def test_openai_api_key_wins_over_azure_env(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
client = OpenAIEmbeddingClient()
assert client.model == "text-embedding-3-small"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint is None
def test_api_version_alone_does_not_override_openai_api_key(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
client = OpenAIEmbeddingClient(api_version="2024-10-21")
assert client.model == "text-embedding-3-small"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint is None
def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
client = OpenAIEmbeddingClient(credential=lambda: "token")
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"]
def test_init_with_credential_wraps_async_token_credential(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
class TestAsyncTokenCredential(AsyncTokenCredential):
async def get_token(self, *scopes: str, **kwargs: object):
raise NotImplementedError
async def close(self) -> None:
pass
async def __aexit__(
self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: TracebackType | None = None,
) -> None:
pass
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
credential = TestAsyncTokenCredential()
token_provider = MagicMock()
with patch("azure.identity.aio.get_bearer_token_provider", return_value=token_provider) as mock_provider:
client = OpenAIEmbeddingClient(credential=cast(Any, credential))
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_MODEL"]
mock_provider.assert_called_once_with(credential, "https://cognitiveservices.azure.com/.default")
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_VERSION"]], indirect=True)
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_MODEL"]
assert client.api_version == "2024-10-21"
def test_openai_base_url_wins_over_azure_aliases(monkeypatch, azure_openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_API_KEY", "test-dummy-key")
monkeypatch.setenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
monkeypatch.setenv("OPENAI_BASE_URL", "https://custom-openai-endpoint.com/v1")
client = OpenAIEmbeddingClient()
assert client.model == "text-embedding-3-small"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint is None
def test_init_with_openai_v1_base_url_and_credential_uses_openai_client(monkeypatch) -> None:
for env in [
"OPENAI_API_KEY",
"OPENAI_ORG_ID",
"OPENAI_MODEL",
"OPENAI_EMBEDDING_MODEL",
"OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_EMBEDDING_MODEL",
"AZURE_OPENAI_MODEL",
"AZURE_OPENAI_API_VERSION",
"AZURE_OPENAI_CHAT_MODEL",
"AZURE_OPENAI_CHAT_COMPLETION_MODEL",
]:
monkeypatch.delenv(env, raising=False)
client = OpenAIEmbeddingClient(
base_url="https://myproject.openai.azure.com/openai/v1/",
model="text-embedding-3-large",
credential=lambda: "fake-token",
)
assert client.model == "text-embedding-3-large"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert isinstance(client.client, AsyncOpenAI)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
assert str(client.client.base_url).rstrip("/").endswith("/openai/v1")
def test_init_with_openai_v1_base_url_and_api_key_uses_openai_client(monkeypatch) -> None:
for env in [
"OPENAI_API_KEY",
"OPENAI_ORG_ID",
"OPENAI_MODEL",
"OPENAI_EMBEDDING_MODEL",
"OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_EMBEDDING_MODEL",
"AZURE_OPENAI_MODEL",
"AZURE_OPENAI_API_VERSION",
"AZURE_OPENAI_CHAT_MODEL",
"AZURE_OPENAI_CHAT_COMPLETION_MODEL",
]:
monkeypatch.delenv(env, raising=False)
# AZURE_OPENAI_BASE_URL + AZURE_OPENAI_API_KEY enter the Azure settings
# path without an explicit endpoint parameter; the /openai/v1 suffix
# should still produce AsyncOpenAI (not AsyncAzureOpenAI).
monkeypatch.setenv("AZURE_OPENAI_BASE_URL", "https://myproject.openai.azure.com/openai/v1/")
monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key")
client = OpenAIEmbeddingClient(model="text-embedding-3-large")
assert client.model == "text-embedding-3-large"
assert not isinstance(client.client, AsyncAzureOpenAI)
assert isinstance(client.client, AsyncOpenAI)
assert str(client.client.base_url).rstrip("/").endswith("/openai/v1")
def test_init_with_azure_endpoint_still_uses_azure_client(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIEmbeddingClient(
azure_endpoint=azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"],
api_key=azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
)
assert isinstance(client.client, AsyncAzureOpenAI)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_get_embeddings() -> None:
async with AzureCliCredential() as credential:
client = _create_azure_embedding_client(credential=cast(Any, credential))
result = await client.get_embeddings(["hello world"])
assert len(result) == 1
assert isinstance(result[0].vector, list)
assert len(result[0].vector) > 0
assert all(isinstance(v, float) for v in result[0].vector)
assert result[0].model is not None
assert result.usage is not None
input_token_count = result.usage["input_token_count"]
assert input_token_count is not None
assert input_token_count > 0
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_get_embeddings_multiple() -> None:
async with AzureCliCredential() as credential:
client = _create_azure_embedding_client(credential=cast(Any, credential))
result = await client.get_embeddings(["hello", "world", "test"])
assert len(result) == 3
dims = [len(embedding.vector) for embedding in result]
assert all(dimension == dims[0] for dimension in dims)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
async def test_azure_openai_get_embeddings_with_dimensions() -> None:
async with AzureCliCredential() as credential:
client = _create_azure_embedding_client(credential=cast(Any, credential))
options: OpenAIEmbeddingOptions = {"dimensions": 256}
result = await client.get_embeddings(["hello world"], options=options)
assert len(result) == 1
assert len(result[0].vector) == 256
@@ -0,0 +1,91 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from types import TracebackType
from typing import Any, cast
from unittest.mock import MagicMock, patch
import pytest
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
from agent_framework_openai._shared import (
AZURE_OPENAI_TOKEN_SCOPE,
_ensure_async_token_provider,
_resolve_azure_credential_to_token_provider,
)
class _AsyncTokenCredentialStub(AsyncTokenCredential):
async def get_token(self, *scopes: str, **kwargs: object):
raise NotImplementedError
async def close(self) -> None:
pass
async def __aexit__(
self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: TracebackType | None = None,
) -> None:
pass
class _TokenCredentialStub(TokenCredential):
def get_token(self, *scopes: str, **kwargs: object):
raise NotImplementedError
def test_resolve_azure_async_credential_wraps_provider() -> None:
credential = _AsyncTokenCredentialStub()
token_provider = MagicMock()
with patch("azure.identity.aio.get_bearer_token_provider", return_value=token_provider) as mock_provider:
resolved = _resolve_azure_credential_to_token_provider(credential)
assert resolved is token_provider
mock_provider.assert_called_once_with(credential, AZURE_OPENAI_TOKEN_SCOPE)
def test_resolve_azure_sync_credential_wraps_provider() -> None:
credential = _TokenCredentialStub()
token_provider = MagicMock()
with patch("azure.identity.get_bearer_token_provider", return_value=token_provider) as mock_provider:
resolved = _resolve_azure_credential_to_token_provider(credential)
assert resolved is token_provider
mock_provider.assert_called_once_with(credential, AZURE_OPENAI_TOKEN_SCOPE)
def test_resolve_azure_callable_token_provider_passthrough() -> None:
token_provider = MagicMock()
assert _resolve_azure_credential_to_token_provider(token_provider) is token_provider
def test_resolve_azure_invalid_credential_raises() -> None:
with pytest.raises(ValueError, match="credential"):
_resolve_azure_credential_to_token_provider(cast(Any, object()))
async def test_ensure_async_token_provider_wraps_sync_provider() -> None:
def sync_provider() -> str:
return "sync-token"
wrapper = _ensure_async_token_provider(sync_provider)
result = await wrapper()
assert result == "sync-token"
async def test_ensure_async_token_provider_wraps_async_provider() -> None:
async def async_provider() -> str:
return "async-token"
wrapper = _ensure_async_token_provider(async_provider)
result = await wrapper()
assert result == "async-token"