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
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:
@@ -0,0 +1,45 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""OpenAI integration for Microsoft Agent Framework.
|
||||
|
||||
This package provides OpenAI client implementations for the Agent Framework,
|
||||
including clients for the Responses API and Chat Completions API.
|
||||
"""
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._chat_client import (
|
||||
OpenAIChatClient,
|
||||
OpenAIChatOptions,
|
||||
OpenAIContinuationToken,
|
||||
RawOpenAIChatClient,
|
||||
)
|
||||
from ._chat_completion_client import (
|
||||
OpenAIChatCompletionClient,
|
||||
OpenAIChatCompletionOptions,
|
||||
RawOpenAIChatCompletionClient,
|
||||
)
|
||||
from ._embedding_client import OpenAIEmbeddingClient, OpenAIEmbeddingOptions
|
||||
from ._exceptions import ContentFilterResultSeverity, OpenAIContentFilterException
|
||||
from ._shared import OpenAISettings
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version("agent-framework-openai")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
__all__ = [
|
||||
"ContentFilterResultSeverity",
|
||||
"OpenAIChatClient",
|
||||
"OpenAIChatCompletionClient",
|
||||
"OpenAIChatCompletionOptions",
|
||||
"OpenAIChatOptions",
|
||||
"OpenAIContentFilterException",
|
||||
"OpenAIContinuationToken",
|
||||
"OpenAIEmbeddingClient",
|
||||
"OpenAIEmbeddingOptions",
|
||||
"OpenAISettings",
|
||||
"RawOpenAIChatClient",
|
||||
"RawOpenAIChatCompletionClient",
|
||||
"__version__",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,500 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import struct
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypedDict, overload
|
||||
|
||||
from agent_framework._clients import BaseEmbeddingClient
|
||||
from agent_framework._settings import SecretString
|
||||
from agent_framework._telemetry import USER_AGENT_KEY
|
||||
from agent_framework._types import Embedding, EmbeddingGenerationOptions, GeneratedEmbeddings, UsageDetails
|
||||
from agent_framework.observability import EmbeddingTelemetryLayer
|
||||
from openai import AsyncAzureOpenAI, AsyncOpenAI
|
||||
|
||||
from ._shared import AzureTokenProvider, load_openai_service_settings
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypeVar # pragma: no cover
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from azure.core.credentials import TokenCredential
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
|
||||
AzureCredentialTypes = TokenCredential | AsyncTokenCredential
|
||||
|
||||
|
||||
DEFAULT_AZURE_OPENAI_EMBEDDING_API_VERSION = "2024-10-21"
|
||||
|
||||
|
||||
class OpenAIEmbeddingOptions(EmbeddingGenerationOptions, total=False):
|
||||
"""OpenAI-specific embedding options.
|
||||
|
||||
Extends EmbeddingGenerationOptions with OpenAI-specific fields.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIEmbeddingOptions
|
||||
|
||||
options: OpenAIEmbeddingOptions = {
|
||||
"model": "text-embedding-3-small",
|
||||
"dimensions": 1536,
|
||||
"encoding_format": "float",
|
||||
}
|
||||
"""
|
||||
|
||||
encoding_format: Literal["float", "base64"]
|
||||
user: str
|
||||
|
||||
|
||||
OpenAIEmbeddingOptionsT = TypeVar(
|
||||
"OpenAIEmbeddingOptionsT",
|
||||
bound=TypedDict, # type: ignore[valid-type]
|
||||
default="OpenAIEmbeddingOptions",
|
||||
covariant=True,
|
||||
)
|
||||
|
||||
|
||||
class RawOpenAIEmbeddingClient(
|
||||
BaseEmbeddingClient[str, list[float], OpenAIEmbeddingOptionsT],
|
||||
Generic[OpenAIEmbeddingOptionsT],
|
||||
):
|
||||
"""Raw OpenAI embedding client without telemetry."""
|
||||
|
||||
INJECTABLE: ClassVar[set[str]] = {"client"}
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None,
|
||||
org_id: str | None = None,
|
||||
base_url: str | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncOpenAI | 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 OpenAI embedding client.
|
||||
|
||||
Keyword Args:
|
||||
model: Embedding model identifier. When not provided, the constructor reads
|
||||
``OPENAI_EMBEDDING_MODEL`` and then ``OPENAI_MODEL``.
|
||||
api_key: API key. When not provided explicitly, the constructor reads
|
||||
``OPENAI_API_KEY``. A callable API key is also supported.
|
||||
org_id: OpenAI organization ID. When not provided explicitly, the constructor reads
|
||||
``OPENAI_ORG_ID``.
|
||||
base_url: Base URL override. When not provided explicitly, the constructor reads
|
||||
``OPENAI_BASE_URL``.
|
||||
default_headers: Additional HTTP headers.
|
||||
async_client: Pre-configured OpenAI client.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
env_file_path: Optional ``.env`` file that is checked before the process environment
|
||||
for ``OPENAI_*`` values.
|
||||
env_file_encoding: Encoding for the ``.env`` file.
|
||||
"""
|
||||
...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
azure_endpoint: str | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
api_version: str | None = None,
|
||||
api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None,
|
||||
base_url: str | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncAzureOpenAI | AsyncOpenAI | 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 OpenAI embedding client.
|
||||
|
||||
Keyword Args:
|
||||
model: Embedding deployment name. When not provided, the constructor reads
|
||||
``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.
|
||||
api_version: Azure API version. When not provided explicitly, the constructor reads
|
||||
``AZURE_OPENAI_API_VERSION`` and then uses the embedding default.
|
||||
api_key: API key. For Azure this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
|
||||
auth. A callable token provider is also accepted, but ``credential`` is the preferred
|
||||
Azure auth surface.
|
||||
base_url: Base URL override. When not provided explicitly, the constructor reads
|
||||
``AZURE_OPENAI_BASE_URL``. Use this instead of ``azure_endpoint`` when you want
|
||||
to pass the full ``.../openai/v1`` base URL directly.
|
||||
default_headers: Additional HTTP headers.
|
||||
async_client: Pre-configured client. Passing ``AsyncAzureOpenAI`` keeps the client on
|
||||
Azure; passing ``AsyncOpenAI`` keeps the client on OpenAI.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
env_file_path: Optional ``.env`` file that is checked before process environment
|
||||
variables for ``AZURE_OPENAI_*`` values.
|
||||
env_file_encoding: Encoding for the ``.env`` file.
|
||||
"""
|
||||
...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
org_id: str | None = None,
|
||||
base_url: str | None = None,
|
||||
azure_endpoint: str | None = None,
|
||||
api_version: str | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncAzureOpenAI | AsyncOpenAI | 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 OpenAI embedding client.
|
||||
|
||||
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_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,
|
||||
but ``credential`` is the preferred Azure auth surface.
|
||||
credential: Azure credential or token provider for Azure OpenAI auth. Passing this
|
||||
is an explicit Azure signal, even when ``OPENAI_API_KEY`` is also configured.
|
||||
Credential objects require the optional ``azure-identity`` package.
|
||||
org_id: OpenAI organization ID. Used only for OpenAI and resolved from
|
||||
``OPENAI_ORG_ID`` when not provided.
|
||||
base_url: Base URL override. For OpenAI this maps to ``OPENAI_BASE_URL``.
|
||||
For Azure this may be used instead of ``azure_endpoint`` when you want
|
||||
to pass the full ``.../openai/v1`` base URL directly.
|
||||
azure_endpoint: Azure resource endpoint. When not provided explicitly, Azure
|
||||
falls back to ``AZURE_OPENAI_ENDPOINT``.
|
||||
api_version: Azure API version to use for Azure requests. When not provided explicitly,
|
||||
Azure falls back to
|
||||
``AZURE_OPENAI_API_VERSION`` and then the embedding default.
|
||||
default_headers: Additional HTTP headers.
|
||||
async_client: Pre-configured client. Passing ``AsyncAzureOpenAI`` keeps the client on
|
||||
Azure; passing ``AsyncOpenAI`` keeps the client on OpenAI.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
env_file_path: Optional ``.env`` file that is checked before process environment
|
||||
variables. The same file is used for both ``OPENAI_*`` and ``AZURE_OPENAI_*``
|
||||
lookups.
|
||||
env_file_encoding: Encoding for the ``.env`` file.
|
||||
|
||||
Notes:
|
||||
Environment resolution precedence is:
|
||||
|
||||
1. Explicit Azure inputs (``azure_endpoint`` or ``credential``)
|
||||
2. Explicit OpenAI API key or ``OPENAI_API_KEY``
|
||||
3. Azure environment fallback
|
||||
|
||||
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_MODEL``,
|
||||
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
|
||||
"""
|
||||
settings, client, use_azure_client = load_openai_service_settings(
|
||||
model=model,
|
||||
api_key=api_key,
|
||||
credential=credential,
|
||||
org_id=org_id,
|
||||
base_url=base_url,
|
||||
endpoint=azure_endpoint,
|
||||
api_version=api_version,
|
||||
default_azure_api_version=DEFAULT_AZURE_OPENAI_EMBEDDING_API_VERSION,
|
||||
default_headers=default_headers,
|
||||
client=async_client,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
openai_model_fields=("embedding_model", "model"),
|
||||
azure_model_fields=("embedding_model", "model"),
|
||||
)
|
||||
|
||||
self.client = client
|
||||
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
|
||||
self.org_id = settings.get("org_id")
|
||||
self.base_url = settings.get("base_url")
|
||||
self.azure_endpoint = settings.get("endpoint")
|
||||
self.api_version = settings.get("api_version")
|
||||
if default_headers:
|
||||
self.default_headers: dict[str, Any] | None = {
|
||||
k: v for k, v in default_headers.items() if k != USER_AGENT_KEY
|
||||
}
|
||||
else:
|
||||
self.default_headers = None
|
||||
if use_azure_client:
|
||||
self.OTEL_PROVIDER_NAME = "azure.ai.openai" # type: ignore[misc]
|
||||
|
||||
super().__init__(additional_properties=additional_properties)
|
||||
|
||||
def service_url(self) -> str:
|
||||
"""Get the URL of the service."""
|
||||
return str(self.client.base_url) if self.client else "Unknown"
|
||||
|
||||
async def get_embeddings(
|
||||
self,
|
||||
values: Sequence[str],
|
||||
*,
|
||||
options: OpenAIEmbeddingOptionsT | None = None,
|
||||
) -> GeneratedEmbeddings[list[float], OpenAIEmbeddingOptionsT]:
|
||||
"""Call the OpenAI embeddings API.
|
||||
|
||||
Args:
|
||||
values: The text values to generate embeddings for.
|
||||
options: Optional embedding generation options.
|
||||
|
||||
Returns:
|
||||
Generated embeddings with usage metadata.
|
||||
|
||||
Raises:
|
||||
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") or self.model
|
||||
if not model:
|
||||
raise ValueError("model is required")
|
||||
|
||||
kwargs: dict[str, Any] = {"input": list(values), "model": model}
|
||||
if dimensions := opts.get("dimensions"):
|
||||
kwargs["dimensions"] = dimensions
|
||||
if encoding_format := opts.get("encoding_format"):
|
||||
kwargs["encoding_format"] = encoding_format
|
||||
if user := opts.get("user"):
|
||||
kwargs["user"] = user
|
||||
|
||||
response = await self.client.embeddings.create(**kwargs)
|
||||
|
||||
encoding = kwargs.get("encoding_format", "float")
|
||||
embeddings: list[Embedding[list[float]]] = []
|
||||
for item in response.data:
|
||||
vector: list[float]
|
||||
if encoding == "base64" and isinstance(item.embedding, str):
|
||||
# Decode base64-encoded floats (little-endian IEEE 754)
|
||||
raw = base64.b64decode(item.embedding)
|
||||
vector = list(struct.unpack(f"<{len(raw) // 4}f", raw))
|
||||
else:
|
||||
vector = item.embedding
|
||||
embeddings.append(
|
||||
Embedding(
|
||||
vector=vector,
|
||||
dimensions=len(vector),
|
||||
model=response.model,
|
||||
)
|
||||
)
|
||||
|
||||
usage_dict: UsageDetails | None = None
|
||||
if response.usage:
|
||||
usage_dict = {
|
||||
"input_token_count": response.usage.prompt_tokens,
|
||||
"total_token_count": response.usage.total_tokens,
|
||||
}
|
||||
|
||||
return GeneratedEmbeddings(embeddings, options=options, usage=usage_dict)
|
||||
|
||||
|
||||
class OpenAIEmbeddingClient(
|
||||
EmbeddingTelemetryLayer[str, list[float], OpenAIEmbeddingOptionsT],
|
||||
RawOpenAIEmbeddingClient[OpenAIEmbeddingOptionsT],
|
||||
Generic[OpenAIEmbeddingOptionsT],
|
||||
):
|
||||
"""OpenAI embedding client with telemetry support."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "openai"
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
api_key: str | Callable[[], str | Awaitable[str]] | None = None,
|
||||
org_id: str | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncOpenAI | None = None,
|
||||
base_url: str | None = None,
|
||||
otel_provider_name: str | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an OpenAI embedding client.
|
||||
|
||||
Keyword Args:
|
||||
model: Embedding model identifier. When not provided, the constructor reads
|
||||
``OPENAI_EMBEDDING_MODEL`` and then ``OPENAI_MODEL``.
|
||||
api_key: API key. When not provided explicitly, the constructor reads
|
||||
``OPENAI_API_KEY``. A callable API key is also supported.
|
||||
org_id: OpenAI organization ID. When not provided explicitly, the constructor reads
|
||||
``OPENAI_ORG_ID``.
|
||||
default_headers: Additional HTTP headers.
|
||||
async_client: Pre-configured OpenAI client.
|
||||
base_url: Base URL override. When not provided explicitly, the constructor reads
|
||||
``OPENAI_BASE_URL``.
|
||||
otel_provider_name: Optional telemetry provider name override.
|
||||
env_file_path: Optional ``.env`` file that is checked before the process environment
|
||||
for ``OPENAI_*`` values.
|
||||
env_file_encoding: Encoding for the ``.env`` file.
|
||||
"""
|
||||
...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
azure_endpoint: str | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
api_version: str | None = None,
|
||||
api_key: str | Callable[[], str | Awaitable[str]] | None = None,
|
||||
base_url: str | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncAzureOpenAI | AsyncOpenAI | None = None,
|
||||
otel_provider_name: str | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an OpenAI embedding client.
|
||||
|
||||
Keyword Args:
|
||||
model: Embedding deployment name. When not provided, the constructor reads
|
||||
``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.
|
||||
api_version: Azure API version. When not provided explicitly, the constructor reads
|
||||
``AZURE_OPENAI_API_VERSION`` and then uses the embedding default.
|
||||
api_key: API key. For Azure this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
|
||||
auth. A callable token provider is also accepted, but ``credential`` is the preferred
|
||||
Azure auth surface.
|
||||
base_url: Base URL override. When not provided explicitly, the constructor reads
|
||||
``AZURE_OPENAI_BASE_URL``. Use this instead of ``azure_endpoint`` when you want
|
||||
to pass the full ``.../openai/v1`` base URL directly.
|
||||
default_headers: Additional HTTP headers.
|
||||
async_client: Pre-configured client. Passing ``AsyncAzureOpenAI`` keeps the client on
|
||||
Azure; passing ``AsyncOpenAI`` keeps the client on OpenAI.
|
||||
otel_provider_name: Optional telemetry provider name override.
|
||||
env_file_path: Optional ``.env`` file that is checked before process environment
|
||||
variables for ``AZURE_OPENAI_*`` values.
|
||||
env_file_encoding: Encoding for the ``.env`` file.
|
||||
"""
|
||||
...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str | None = None,
|
||||
api_key: str | Callable[[], str | Awaitable[str]] | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
org_id: str | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncAzureOpenAI | AsyncOpenAI | None = None,
|
||||
base_url: str | None = None,
|
||||
azure_endpoint: str | None = None,
|
||||
api_version: str | None = None,
|
||||
otel_provider_name: str | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an OpenAI embedding client.
|
||||
|
||||
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_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,
|
||||
but ``credential`` is the preferred Azure auth surface.
|
||||
credential: Azure credential or token provider for Azure OpenAI auth. Passing this
|
||||
is an explicit Azure signal, even when ``OPENAI_API_KEY`` is also configured.
|
||||
Credential objects require the optional ``azure-identity`` package.
|
||||
org_id: OpenAI organization ID. Used only for OpenAI and resolved from
|
||||
``OPENAI_ORG_ID`` when not provided.
|
||||
default_headers: Additional HTTP headers.
|
||||
async_client: Pre-configured client. Passing ``AsyncAzureOpenAI`` keeps the client on
|
||||
Azure; passing ``AsyncOpenAI`` keeps the client on OpenAI.
|
||||
base_url: Base URL override. For OpenAI this maps to ``OPENAI_BASE_URL``.
|
||||
For Azure this may be used instead of ``azure_endpoint`` when you want
|
||||
to pass the full ``.../openai/v1`` base URL directly.
|
||||
azure_endpoint: Azure resource endpoint. When not provided explicitly, Azure
|
||||
falls back to ``AZURE_OPENAI_ENDPOINT``.
|
||||
api_version: Azure API version to use for Azure requests. When not provided explicitly,
|
||||
Azure falls back to
|
||||
``AZURE_OPENAI_API_VERSION`` and then the embedding default.
|
||||
otel_provider_name: Override the OpenTelemetry provider name.
|
||||
env_file_path: Optional ``.env`` file that is checked before process environment
|
||||
variables. The same file is used for both ``OPENAI_*`` and ``AZURE_OPENAI_*``
|
||||
lookups.
|
||||
env_file_encoding: Encoding for the ``.env`` file.
|
||||
|
||||
Notes:
|
||||
Environment resolution precedence is:
|
||||
|
||||
1. Explicit Azure inputs (``azure_endpoint`` or ``credential``)
|
||||
2. Explicit OpenAI API key or ``OPENAI_API_KEY``
|
||||
3. Azure environment fallback
|
||||
|
||||
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_MODEL``,
|
||||
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIEmbeddingClient
|
||||
|
||||
# Using environment variables
|
||||
# Set OPENAI_API_KEY=sk-...
|
||||
# Set OPENAI_EMBEDDING_MODEL=text-embedding-3-small
|
||||
client = OpenAIEmbeddingClient()
|
||||
|
||||
# Or passing OpenAI parameters directly
|
||||
client = OpenAIEmbeddingClient(
|
||||
model="text-embedding-3-small",
|
||||
api_key="sk-...",
|
||||
)
|
||||
|
||||
# Or using Azure OpenAI with an Azure credential
|
||||
client = OpenAIEmbeddingClient(
|
||||
model="text-embedding-3-small",
|
||||
azure_endpoint="https://example-resource.openai.azure.com/",
|
||||
credential=my_azure_credential,
|
||||
)
|
||||
"""
|
||||
super().__init__(
|
||||
model=model,
|
||||
api_key=api_key,
|
||||
credential=credential,
|
||||
org_id=org_id,
|
||||
base_url=base_url,
|
||||
azure_endpoint=azure_endpoint,
|
||||
api_version=api_version,
|
||||
default_headers=default_headers,
|
||||
async_client=async_client,
|
||||
otel_provider_name=otel_provider_name,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from agent_framework.exceptions import ChatClientContentFilterException
|
||||
from openai import BadRequestError
|
||||
|
||||
|
||||
class ContentFilterResultSeverity(Enum):
|
||||
"""The severity of the content filter result."""
|
||||
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
SAFE = "safe"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContentFilterResult:
|
||||
"""The result of a content filter check."""
|
||||
|
||||
filtered: bool = False
|
||||
detected: bool = False
|
||||
severity: ContentFilterResultSeverity = ContentFilterResultSeverity.SAFE
|
||||
|
||||
@classmethod
|
||||
def from_inner_error_result(cls, inner_error_results: dict[str, Any]) -> ContentFilterResult:
|
||||
"""Creates a ContentFilterResult from the inner error results.
|
||||
|
||||
Args:
|
||||
inner_error_results: The inner error results.
|
||||
|
||||
Returns:
|
||||
ContentFilterResult: The ContentFilterResult.
|
||||
"""
|
||||
return cls(
|
||||
filtered=inner_error_results.get("filtered", False),
|
||||
detected=inner_error_results.get("detected", False),
|
||||
severity=ContentFilterResultSeverity(
|
||||
inner_error_results.get("severity", ContentFilterResultSeverity.SAFE.value)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ContentFilterCodes(Enum):
|
||||
"""Content filter codes."""
|
||||
|
||||
RESPONSIBLE_AI_POLICY_VIOLATION = "ResponsibleAIPolicyViolation"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenAIContentFilterException(ChatClientContentFilterException):
|
||||
"""AI exception for an error from Azure OpenAI's content filter."""
|
||||
|
||||
# The parameter that caused the error.
|
||||
param: str | None
|
||||
|
||||
# The error code specific to the content filter.
|
||||
content_filter_code: ContentFilterCodes
|
||||
|
||||
# The results of the different content filter checks.
|
||||
content_filter_result: dict[str, ContentFilterResult]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
inner_exception: BadRequestError,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the ContentFilterAIException class.
|
||||
|
||||
Args:
|
||||
message: The error message.
|
||||
inner_exception: The inner exception.
|
||||
"""
|
||||
super().__init__(message)
|
||||
|
||||
self.param = inner_exception.param
|
||||
if inner_exception.body is not None and isinstance(inner_exception.body, dict):
|
||||
inner_error = inner_exception.body.get("innererror", {}) # type: ignore
|
||||
self.content_filter_code = ContentFilterCodes(
|
||||
inner_error.get("code", ContentFilterCodes.RESPONSIBLE_AI_POLICY_VIOLATION.value) # type: ignore
|
||||
)
|
||||
self.content_filter_result = {
|
||||
key: ContentFilterResult.from_inner_error_result(values) # type: ignore
|
||||
for key, values in inner_error.get("content_filter_result", {}).items() # type: ignore
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
|
||||
from agent_framework._settings import SecretString, load_settings
|
||||
from agent_framework._telemetry import APP_INFO, prepend_agent_framework_to_user_agent
|
||||
from agent_framework.exceptions import SettingNotFoundError
|
||||
from openai import AsyncAzureOpenAI, AsyncOpenAI, AsyncStream, _legacy_response # type: ignore
|
||||
from openai.types import Completion
|
||||
from openai.types.audio import Transcription
|
||||
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
||||
from openai.types.images_response import ImagesResponse
|
||||
from openai.types.responses.response import Response
|
||||
from openai.types.responses.response_stream_event import ResponseStreamEvent
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypedDict # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypedDict # pragma: no cover
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from azure.core.credentials import TokenCredential
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
|
||||
AzureCredentialTypes = TokenCredential | AsyncTokenCredential
|
||||
|
||||
|
||||
AZURE_OPENAI_TOKEN_SCOPE = "https://cognitiveservices.azure.com/.default" # noqa: S105 # nosec B105
|
||||
|
||||
|
||||
RESPONSE_TYPE = Union[
|
||||
ChatCompletion,
|
||||
Completion,
|
||||
AsyncStream[ChatCompletionChunk],
|
||||
AsyncStream[Completion],
|
||||
list[Any],
|
||||
ImagesResponse,
|
||||
Response,
|
||||
AsyncStream[ResponseStreamEvent],
|
||||
Transcription,
|
||||
_legacy_response.HttpxBinaryResponseContent,
|
||||
]
|
||||
|
||||
AzureTokenProvider = Callable[[], str | Awaitable[str]]
|
||||
|
||||
|
||||
class OpenAISettings(TypedDict, total=False):
|
||||
"""OpenAI environment settings.
|
||||
|
||||
Settings are resolved in this order: explicit keyword arguments, values from an
|
||||
explicitly provided .env file, then environment variables with the prefix
|
||||
'OPENAI_'. If settings are missing after resolution, validation will fail.
|
||||
|
||||
Keyword Args:
|
||||
api_key: OpenAI API key, see https://platform.openai.com/account/api-keys.
|
||||
Can be set via environment variable OPENAI_API_KEY.
|
||||
base_url: The base URL for the OpenAI API.
|
||||
Can be set via environment variable OPENAI_BASE_URL.
|
||||
org_id: This is usually optional unless your account belongs to multiple organizations.
|
||||
Can be set via environment variable OPENAI_ORG_ID.
|
||||
model: The OpenAI model to use, for example, gpt-4o or o1.
|
||||
Can be set via environment variable OPENAI_MODEL.
|
||||
embedding_model: The OpenAI embedding model to use, for example, text-embedding-3-small.
|
||||
Can be set via environment variable OPENAI_EMBEDDING_MODEL.
|
||||
chat_model: The OpenAIChatClient model to prefer before OPENAI_MODEL.
|
||||
Can be set via environment variable OPENAI_CHAT_MODEL.
|
||||
chat_completion_model: The OpenAIChatCompletionClient model to prefer before OPENAI_MODEL.
|
||||
Can be set via environment variable OPENAI_CHAT_COMPLETION_MODEL.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAISettings
|
||||
|
||||
# Using environment variables
|
||||
# Set OPENAI_API_KEY=sk-...
|
||||
# Set OPENAI_MODEL=gpt-4o
|
||||
settings = load_settings(OpenAISettings, env_prefix="OPENAI_")
|
||||
|
||||
# Or passing parameters directly
|
||||
settings = load_settings(OpenAISettings, env_prefix="OPENAI_", api_key="sk-...", model="gpt-4o")
|
||||
|
||||
# Or loading from a .env file
|
||||
settings = load_settings(OpenAISettings, env_prefix="OPENAI_", env_file_path="path/to/.env")
|
||||
"""
|
||||
|
||||
api_key: SecretString | None
|
||||
base_url: str | None
|
||||
org_id: str | None
|
||||
model: str | None
|
||||
embedding_model: str | None
|
||||
chat_model: str | None
|
||||
chat_completion_model: str | None
|
||||
|
||||
|
||||
class AzureOpenAISettings(TypedDict, total=False):
|
||||
"""Azure OpenAI environment settings."""
|
||||
|
||||
endpoint: str | None
|
||||
base_url: str | None
|
||||
api_key: SecretString | None
|
||||
model: str | None
|
||||
embedding_model: str | None
|
||||
chat_model: str | None
|
||||
chat_completion_model: str | None
|
||||
api_version: str | None
|
||||
|
||||
|
||||
OpenAIModelSettingName = Literal["model", "embedding_model", "chat_model", "chat_completion_model"]
|
||||
|
||||
OPENAI_MODEL_ENV_VARS: dict[OpenAIModelSettingName, str] = {
|
||||
"model": "OPENAI_MODEL",
|
||||
"embedding_model": "OPENAI_EMBEDDING_MODEL",
|
||||
"chat_model": "OPENAI_CHAT_MODEL",
|
||||
"chat_completion_model": "OPENAI_CHAT_COMPLETION_MODEL",
|
||||
}
|
||||
|
||||
AZURE_MODEL_ENV_VARS: dict[OpenAIModelSettingName, str] = {
|
||||
"model": "AZURE_OPENAI_MODEL",
|
||||
"embedding_model": "AZURE_OPENAI_EMBEDDING_MODEL",
|
||||
"chat_model": "AZURE_OPENAI_CHAT_MODEL",
|
||||
"chat_completion_model": "AZURE_OPENAI_CHAT_COMPLETION_MODEL",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_named_setting(
|
||||
settings: Mapping[str, Any],
|
||||
fields: Sequence[OpenAIModelSettingName],
|
||||
) -> str | None:
|
||||
"""Return the first populated value from ``fields``."""
|
||||
for field in fields:
|
||||
value = settings.get(field)
|
||||
if isinstance(value, str) and value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _join_env_names(env_names: Sequence[str]) -> str:
|
||||
"""Format env var names for user-facing error messages."""
|
||||
return ", ".join(f"'{env_name}'" for env_name in env_names)
|
||||
|
||||
|
||||
def load_openai_service_settings(
|
||||
*,
|
||||
model: str | None,
|
||||
api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None,
|
||||
org_id: str | None,
|
||||
base_url: str | None,
|
||||
endpoint: str | None,
|
||||
api_version: str | None,
|
||||
default_azure_api_version: str,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
client: AsyncOpenAI | None = None,
|
||||
env_file_path: str | None,
|
||||
env_file_encoding: str | None,
|
||||
openai_model_fields: Sequence[OpenAIModelSettingName] = ("model",),
|
||||
azure_model_fields: Sequence[OpenAIModelSettingName] = ("model",),
|
||||
responses_mode: bool = False,
|
||||
timeout: float | None = None,
|
||||
) -> tuple[dict[str, Any], AsyncOpenAI, bool]:
|
||||
"""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
|
||||
(``endpoint`` or ``credential``) or when no explicit
|
||||
OpenAI API key is available.
|
||||
"""
|
||||
# Merge APP_INFO into the headers
|
||||
merged_headers = dict(copy(default_headers)) if default_headers else {}
|
||||
if APP_INFO:
|
||||
merged_headers.update(APP_INFO)
|
||||
merged_headers = prepend_agent_framework_to_user_agent(merged_headers)
|
||||
|
||||
api_key_callable = api_key if callable(api_key) else None
|
||||
api_key_str = api_key if not callable(api_key) else None
|
||||
azure_client = isinstance(client, AsyncAzureOpenAI)
|
||||
use_azure = azure_client or endpoint is not None or credential is not None
|
||||
checked_openai = False
|
||||
if not use_azure:
|
||||
openai_settings_kwargs: dict[str, Any] = {
|
||||
"api_key": api_key_str,
|
||||
"org_id": org_id,
|
||||
"base_url": base_url,
|
||||
"env_file_path": env_file_path,
|
||||
"env_file_encoding": env_file_encoding,
|
||||
}
|
||||
if model is not None:
|
||||
openai_settings_kwargs[openai_model_fields[0]] = model
|
||||
openai_settings = load_settings(
|
||||
OpenAISettings,
|
||||
env_prefix="OPENAI_",
|
||||
**openai_settings_kwargs,
|
||||
)
|
||||
if resolved_model := _resolve_named_setting(openai_settings, openai_model_fields):
|
||||
openai_settings["model"] = resolved_model
|
||||
if client:
|
||||
return openai_settings, client, False # type: ignore[return-value]
|
||||
if openai_settings.get("api_key") is not None or api_key_callable is not None:
|
||||
resolved_model = _resolve_named_setting(openai_settings, openai_model_fields)
|
||||
if not resolved_model:
|
||||
raise SettingNotFoundError(
|
||||
"Model must be specified via the 'model' parameter or the "
|
||||
f"{_join_env_names([OPENAI_MODEL_ENV_VARS[field] for field in openai_model_fields])} "
|
||||
"environment variable."
|
||||
)
|
||||
|
||||
client_args: dict[str, Any] = {
|
||||
"api_key": api_key_callable
|
||||
if api_key_callable is not None
|
||||
else openai_settings["api_key"].get_secret_value(), # type: ignore[reportOptionalMemberAccess, union-attr]
|
||||
"organization": openai_settings.get("org_id"),
|
||||
"default_headers": merged_headers,
|
||||
}
|
||||
if base_url := openai_settings.get("base_url"):
|
||||
client_args["base_url"] = base_url
|
||||
if timeout is not None:
|
||||
client_args["timeout"] = timeout
|
||||
return openai_settings, AsyncOpenAI(**client_args), False # type: ignore[return-value]
|
||||
checked_openai = True
|
||||
azure_settings = load_settings(
|
||||
AzureOpenAISettings,
|
||||
env_prefix="AZURE_OPENAI_",
|
||||
required_fields=None if client else [("base_url", "endpoint")],
|
||||
api_key=api_key_str,
|
||||
endpoint=endpoint,
|
||||
base_url=base_url,
|
||||
api_version=api_version or default_azure_api_version,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
if model is not None:
|
||||
azure_settings[azure_model_fields[0]] = model
|
||||
client_args = {}
|
||||
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_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_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
|
||||
or azure_settings.get("base_url") is not None
|
||||
)
|
||||
if checked_openai and not has_azure_configuration:
|
||||
raise SettingNotFoundError(
|
||||
"OpenAI credentials are required. Provide the 'api_key' parameter or set 'OPENAI_API_KEY'. "
|
||||
"To use Azure OpenAI instead, pass 'azure_endpoint' or set 'AZURE_OPENAI_ENDPOINT' or "
|
||||
"'AZURE_OPENAI_BASE_URL'."
|
||||
)
|
||||
raise SettingNotFoundError(
|
||||
"Azure OpenAI client requires a model, which can be provided via the 'model' parameter, "
|
||||
f"or the {deployment_env_guidance} environment variable."
|
||||
)
|
||||
if client:
|
||||
return azure_settings, client, True # type: ignore[return-value]
|
||||
client_args["default_headers"] = merged_headers
|
||||
if endpoint := azure_settings.get("endpoint"):
|
||||
if responses_mode:
|
||||
client_args["base_url"] = f"{endpoint.rstrip('/')}/openai/v1/"
|
||||
else:
|
||||
client_args["azure_endpoint"] = endpoint
|
||||
if base_url := azure_settings.get("base_url"):
|
||||
client_args["base_url"] = base_url
|
||||
if api_key := azure_settings.get("api_key"):
|
||||
client_args["api_key"] = api_key.get_secret_value()
|
||||
if api_key_callable:
|
||||
client_args["api_key"] = api_key_callable
|
||||
if api_version := azure_settings.get("api_version"):
|
||||
client_args["api_version"] = api_version
|
||||
if credential:
|
||||
client_args["azure_ad_token_provider"] = _resolve_azure_credential_to_token_provider(credential)
|
||||
if "api_key" not in client_args and "azure_ad_token_provider" not in client_args:
|
||||
raise SettingNotFoundError(
|
||||
"Azure OpenAI client requires either an API key or an Azure AD token provider."
|
||||
" This can be provided either as a callable api_key or via the credential parameter."
|
||||
)
|
||||
|
||||
# The /openai/v1 endpoint exposes an OpenAI-compatible API surface.
|
||||
# AsyncAzureOpenAI rewrites certain request paths (e.g. /embeddings,
|
||||
# /chat/completions) by inserting /deployments/{model}/, which produces
|
||||
# 404s on this endpoint. Use AsyncOpenAI instead so request URLs are
|
||||
# sent as-is. responses_mode is excluded because the Responses API path
|
||||
# (/responses) is not rewritten by the Azure SDK.
|
||||
resolved_base_url = client_args.get("base_url", "")
|
||||
if not responses_mode and resolved_base_url and resolved_base_url.rstrip("/").endswith("/openai/v1"):
|
||||
openai_args: dict[str, Any] = {
|
||||
"base_url": resolved_base_url,
|
||||
"default_headers": client_args.get("default_headers"),
|
||||
}
|
||||
if "azure_ad_token_provider" in client_args:
|
||||
openai_args["api_key"] = _ensure_async_token_provider(client_args["azure_ad_token_provider"])
|
||||
elif "api_key" in client_args:
|
||||
openai_args["api_key"] = client_args["api_key"]
|
||||
if timeout is not None:
|
||||
openai_args["timeout"] = timeout
|
||||
return azure_settings, AsyncOpenAI(**openai_args), True # type: ignore[return-value]
|
||||
|
||||
if timeout is not None:
|
||||
client_args["timeout"] = timeout
|
||||
return azure_settings, AsyncAzureOpenAI(**client_args), True # type: ignore[return-value]
|
||||
|
||||
|
||||
def _ensure_async_token_provider(
|
||||
provider: AzureTokenProvider,
|
||||
) -> Callable[[], Awaitable[str]]:
|
||||
"""Wrap a (possibly synchronous) token provider so it always returns an awaitable.
|
||||
|
||||
``AsyncOpenAI`` requires callable ``api_key`` values to return ``Awaitable[str]``.
|
||||
Azure token providers may return a plain ``str``, so this normalises them.
|
||||
"""
|
||||
|
||||
async def _wrapper() -> str:
|
||||
result = provider()
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
return await result
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
def _resolve_azure_credential_to_token_provider(
|
||||
credential: AzureCredentialTypes | AzureTokenProvider,
|
||||
) -> AzureTokenProvider:
|
||||
"""Resolve an Azure credential or token provider for Azure OpenAI auth."""
|
||||
if callable(credential):
|
||||
return credential
|
||||
|
||||
try:
|
||||
from azure.core.credentials import TokenCredential
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
from azure.identity import get_bearer_token_provider
|
||||
from azure.identity.aio import get_bearer_token_provider as get_async_bearer_token_provider
|
||||
except ModuleNotFoundError as exc:
|
||||
raise ModuleNotFoundError(
|
||||
"Azure credential auth requires the 'azure-identity' package. Install it with: pip install azure-identity"
|
||||
) from exc
|
||||
|
||||
if isinstance(credential, AsyncTokenCredential):
|
||||
return get_async_bearer_token_provider(credential, AZURE_OPENAI_TOKEN_SCOPE)
|
||||
if isinstance(credential, TokenCredential):
|
||||
return get_bearer_token_provider(credential, AZURE_OPENAI_TOKEN_SCOPE)
|
||||
raise ValueError(
|
||||
"The 'credential' parameter must be an Azure TokenCredential, AsyncTokenCredential, or a "
|
||||
"callable token provider."
|
||||
)
|
||||
|
||||
|
||||
def maybe_append_azure_endpoint_guidance(message: str, *, azure_endpoint: str | None) -> str:
|
||||
"""Append Azure endpoint guidance only when the configured endpoint shape looks suspicious."""
|
||||
if not azure_endpoint or not azure_endpoint.rstrip("/").endswith("/openai/v1"):
|
||||
return message
|
||||
|
||||
return (
|
||||
f"{message} If you are using Azure OpenAI key auth, pass the resource endpoint without "
|
||||
"'/openai/v1' to 'azure_endpoint', or pass the full '/openai/v1' URL via 'base_url' instead."
|
||||
)
|
||||
|
||||
|
||||
def get_api_key(
|
||||
api_key: str | SecretString | Callable[[], str | Awaitable[str]] | None,
|
||||
) -> str | Callable[[], str | Awaitable[str]] | None:
|
||||
"""Get the appropriate API key value for client initialization.
|
||||
|
||||
Args:
|
||||
api_key: The API key parameter which can be a string, SecretString, callable, or None.
|
||||
|
||||
Returns:
|
||||
For callable API keys: returns the callable directly.
|
||||
For SecretString: returns the unwrapped secret value.
|
||||
For string/None API keys: returns as-is.
|
||||
"""
|
||||
if isinstance(api_key, SecretString):
|
||||
return api_key.get_secret_value()
|
||||
|
||||
return api_key # Pass callable, string, or None directly to OpenAI SDK
|
||||
Reference in New Issue
Block a user