Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-llm/src/lexigram/ai/llm/clients/azure_openai.py: 26%
35 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""Azure OpenAI LLM client for the Lexigram LLM routing system.
3Extends :class:`~lexigram.ai.llm.clients.openai.OpenAIClient` to target the
4Azure OpenAI Service endpoint instead of ``api.openai.com``.
6Azure OpenAI uses deployment-specific URLs of the form::
8 https://{resource}.openai.azure.com/openai/deployments/{deployment}/
10and requires an ``api-version`` query parameter on every request, so this
11client overrides the ``AsyncOpenAI`` constructor to point at the correct base
12URL and injects the required headers.
14Configuration is sourced from ``ClientConfig.extra``:
16* ``azure_resource`` — Azure resource name (required)
17* ``azure_deployment`` — deployment / model name inside the resource (required)
18* ``azure_api_version`` — API version string (default: ``2024-02-15-preview``)
20``ClientConfig.api_key`` must carry the Azure OpenAI API key (or leave it as
21``None`` to fall back on Azure AD credential injection through the ``openai``
22SDK's ``azure_ad_token_provider`` mechanism — not wired here by default).
23"""
25from __future__ import annotations
27from typing import TYPE_CHECKING, Any
29from lexigram.ai.llm.clients.openai import OpenAIClient
30from lexigram.contracts.core import HealthCheckResult, HealthStatus
32if TYPE_CHECKING:
33 from lexigram.ai.llm.config import ClientConfig
35__all__ = ["AzureOpenAIClient"]
37_DEFAULT_API_VERSION = "2024-02-15-preview"
40class AzureOpenAIClient(OpenAIClient):
41 """Azure OpenAI Service client extending the core :class:`OpenAIClient`.
43 Routes all requests through the Azure OpenAI REST endpoint rather than
44 the public OpenAI API. All streaming, tool-calling, and retry semantics
45 are inherited from :class:`OpenAIClient` unchanged.
47 Args:
48 config: LLM configuration. ``config.extra`` must contain
49 ``azure_resource`` and ``azure_deployment``. May optionally
50 contain ``azure_api_version``.
51 """
53 def __init__(self, config: ClientConfig) -> None:
54 """Initialise the Azure OpenAI client.
56 Builds the deployment-scoped Azure endpoint URL and configures the
57 ``AsyncAzureOpenAI`` client from the ``openai`` SDK.
59 Args:
60 config: LLM configuration including Azure-specific ``extra`` keys.
62 Raises:
63 ImportError: If the ``openai`` package is not installed.
64 ValueError: If ``azure_resource`` or ``azure_deployment`` are
65 missing from ``config.extra``.
66 """
67 extra: dict[str, Any] = config.extra or {}
68 resource: str = extra.get("azure_resource", "")
69 deployment: str = extra.get("azure_deployment", "") or config.model
70 api_version: str = extra.get("azure_api_version", _DEFAULT_API_VERSION)
72 if not resource:
73 raise ValueError(
74 "AzureOpenAIClient requires 'azure_resource' in ClientConfig.extra"
75 )
76 if not deployment:
77 raise ValueError(
78 "AzureOpenAIClient requires 'azure_deployment' in ClientConfig.extra "
79 "or a non-empty config.model"
80 )
82 self._azure_resource = resource
83 self._azure_deployment = deployment
84 self._azure_api_version = api_version
86 # Patch config to use the Azure endpoint so AbstractLLMClient metrics
87 # carry the correct base URL.
88 azure_base = (
89 f"https://{resource}.openai.azure.com/openai/deployments/{deployment}"
90 )
91 patched = config.model_copy(
92 update={"api_base": azure_base, "model": deployment}
93 )
95 try:
96 from openai import AsyncAzureOpenAI
97 except ImportError as exc:
98 raise ImportError(
99 "AzureOpenAIClient requires the 'openai' package. "
100 "Install with: pip install lexigram-ai-llm[openai]"
101 ) from exc
103 # Call AbstractLLMClient.__init__ directly, bypassing OpenAIClient.__init__,
104 # so we can build the Azure client instead of the public OpenAI client.
105 from lexigram.ai.llm.clients.base import AbstractLLMClient
107 AbstractLLMClient.__init__(self, config=patched)
109 api_key = config.api_key.get_secret_value() if config.api_key else None
110 self.client = AsyncAzureOpenAI(
111 api_key=api_key,
112 azure_endpoint=f"https://{resource}.openai.azure.com",
113 azure_deployment=deployment,
114 api_version=api_version,
115 timeout=config.timeout,
116 )
118 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
119 """Perform a lightweight health check against the Azure deployment.
121 Attempts to list models from the Azure endpoint as a connectivity
122 probe. A successful response (even empty) returns HEALTHY.
124 Args:
125 timeout: Informational — the underlying client timeout applies.
127 Returns:
128 Structured :class:`~lexigram.contracts.core.health.HealthCheckResult`.
129 """
130 try:
131 await self.client.models.list()
132 except Exception as exc: # noqa: BLE001 - transport/SDK errors are provider-specific
133 return HealthCheckResult(
134 component="llm.azure_openai",
135 status=HealthStatus.UNHEALTHY,
136 error=str(exc),
137 details={
138 "resource": self._azure_resource,
139 "deployment": self._azure_deployment,
140 "api_version": self._azure_api_version,
141 },
142 )
144 return HealthCheckResult(
145 component="llm.azure_openai",
146 status=HealthStatus.HEALTHY,
147 details={
148 "resource": self._azure_resource,
149 "deployment": self._azure_deployment,
150 "api_version": self._azure_api_version,
151 },
152 )