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

1"""Azure OpenAI LLM client for the Lexigram LLM routing system. 

2 

3Extends :class:`~lexigram.ai.llm.clients.openai.OpenAIClient` to target the 

4Azure OpenAI Service endpoint instead of ``api.openai.com``. 

5 

6Azure OpenAI uses deployment-specific URLs of the form:: 

7 

8 https://{resource}.openai.azure.com/openai/deployments/{deployment}/ 

9 

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. 

13 

14Configuration is sourced from ``ClientConfig.extra``: 

15 

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``) 

19 

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""" 

24 

25from __future__ import annotations 

26 

27from typing import TYPE_CHECKING, Any 

28 

29from lexigram.ai.llm.clients.openai import OpenAIClient 

30from lexigram.contracts.core import HealthCheckResult, HealthStatus 

31 

32if TYPE_CHECKING: 

33 from lexigram.ai.llm.config import ClientConfig 

34 

35__all__ = ["AzureOpenAIClient"] 

36 

37_DEFAULT_API_VERSION = "2024-02-15-preview" 

38 

39 

40class AzureOpenAIClient(OpenAIClient): 

41 """Azure OpenAI Service client extending the core :class:`OpenAIClient`. 

42 

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. 

46 

47 Args: 

48 config: LLM configuration. ``config.extra`` must contain 

49 ``azure_resource`` and ``azure_deployment``. May optionally 

50 contain ``azure_api_version``. 

51 """ 

52 

53 def __init__(self, config: ClientConfig) -> None: 

54 """Initialise the Azure OpenAI client. 

55 

56 Builds the deployment-scoped Azure endpoint URL and configures the 

57 ``AsyncAzureOpenAI`` client from the ``openai`` SDK. 

58 

59 Args: 

60 config: LLM configuration including Azure-specific ``extra`` keys. 

61 

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) 

71 

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 ) 

81 

82 self._azure_resource = resource 

83 self._azure_deployment = deployment 

84 self._azure_api_version = api_version 

85 

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 ) 

94 

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 

102 

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 

106 

107 AbstractLLMClient.__init__(self, config=patched) 

108 

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 ) 

117 

118 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult: 

119 """Perform a lightweight health check against the Azure deployment. 

120 

121 Attempts to list models from the Azure endpoint as a connectivity 

122 probe. A successful response (even empty) returns HEALTHY. 

123 

124 Args: 

125 timeout: Informational — the underlying client timeout applies. 

126 

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 ) 

143 

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 )