Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-llm/src/lexigram/ai/llm/clients/openai_compatible.py: 56%

34 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

1"""OpenAI-compatible LLM client base for third-party providers. 

2 

3Many providers (DeepSeek, Together AI, Fireworks AI, etc.) expose an 

4OpenAI-compatible REST API. This module provides an intermediate base class, 

5:class:`OpenAICompatibleClient`, that re-uses all logic from 

6:class:`~lexigram.ai.llm.clients.openai.OpenAIClient` while pointing the 

7``AsyncOpenAI`` client at a different ``base_url``. 

8 

9Concrete subclasses only need to override :attr:`_provider_base_url` and 

10:attr:`_provider_name`; all other behaviour (streaming, tool calling, retry, 

11error handling) is inherited unchanged. 

12""" 

13 

14from __future__ import annotations 

15 

16from typing import TYPE_CHECKING 

17 

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

19from lexigram.contracts.core import HealthCheckResult, HealthStatus 

20 

21if TYPE_CHECKING: 

22 from lexigram.ai.llm.config import ClientConfig 

23 

24__all__ = [ 

25 "DeepSeekClient", 

26 "FireworksClient", 

27 "OpenAICompatibleClient", 

28 "TogetherClient", 

29] 

30 

31 

32class OpenAICompatibleClient(OpenAIClient): 

33 """Thin base class for providers that expose an OpenAI-compatible API. 

34 

35 Subclasses must declare :attr:`_provider_base_url` (the base URL of the 

36 provider's ``/v1`` endpoint) and :attr:`_provider_name` (used in health 

37 check payloads and log messages). 

38 

39 The ``AsyncOpenAI`` client from the ``openai`` SDK is configured to send 

40 requests to the provider's base URL with ``Bearer`` authentication using 

41 ``ClientConfig.api_key``. 

42 """ 

43 

44 _provider_base_url: str = "" 

45 _provider_name: str = "openai_compatible" 

46 

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

48 """Initialise the OpenAI-compatible client. 

49 

50 Overrides the base URL used by the ``AsyncOpenAI`` client to point at 

51 the third-party provider's endpoint. 

52 

53 Args: 

54 config: LLM configuration. ``config.api_key`` must contain the 

55 provider's API key. ``config.api_base``, when set, takes 

56 precedence over :attr:`_provider_base_url`. 

57 

58 Raises: 

59 ImportError: If the ``openai`` package is not installed. 

60 """ 

61 base_url = config.api_base or self._provider_base_url 

62 

63 try: 

64 from openai import AsyncOpenAI 

65 except ImportError as exc: 

66 raise ImportError( 

67 f"{type(self).__name__} requires the 'openai' package. " 

68 "Install with: pip install lexigram-ai-llm[openai]" 

69 ) from exc 

70 

71 from lexigram.ai.llm.clients.base import AbstractLLMClient 

72 

73 AbstractLLMClient.__init__(self, config=config) 

74 

75 api_key = ( 

76 config.api_key.get_secret_value() 

77 if config.api_key is not None 

78 else "sk-placeholder" 

79 ) 

80 self.client = AsyncOpenAI( 

81 api_key=api_key, 

82 base_url=base_url, 

83 timeout=config.timeout, 

84 ) 

85 

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

87 """Probe the provider's ``/models`` endpoint for connectivity. 

88 

89 Args: 

90 timeout: Informational only; the SDK-level timeout governs the 

91 actual request duration. 

92 

93 Returns: 

94 Structured :class:`~lexigram.contracts.core.health.HealthCheckResult`. 

95 """ 

96 try: 

97 models = await self.client.models.list() 

98 first_model: str = models.data[0].id if models.data else "" 

99 except Exception as exc: # noqa: BLE001 - transport/SDK errors are provider-specific 

100 return HealthCheckResult( 

101 component=f"llm.{self._provider_name}", 

102 status=HealthStatus.UNHEALTHY, 

103 error=str(exc), 

104 details={"model": self.config.model}, 

105 ) 

106 

107 return HealthCheckResult( 

108 component=f"llm.{self._provider_name}", 

109 status=HealthStatus.HEALTHY, 

110 details={"model": self.config.model, "probe_model": first_model}, 

111 ) 

112 

113 

114# ────────────────────────────────────────────────────────────────────── 

115# Concrete provider subclasses 

116# ────────────────────────────────────────────────────────────────────── 

117 

118 

119class DeepSeekClient(OpenAICompatibleClient): 

120 """LLM client for the DeepSeek API (OpenAI-compatible). 

121 

122 Targets ``https://api.deepseek.com/v1``. Authentication uses a Bearer 

123 token supplied as ``ClientConfig.api_key``. 

124 

125 Supported models include ``deepseek-chat`` and ``deepseek-coder``. 

126 

127 Example: 

128 >>> config = ClientConfig( 

129 ... provider="deepseek", 

130 ... model="deepseek-chat", 

131 ... api_key=SecretStr("sk-..."), 

132 ... ) 

133 >>> client = DeepSeekClient(config) 

134 """ 

135 

136 _provider_base_url = "https://api.deepseek.com/v1" 

137 _provider_name = "deepseek" 

138 

139 

140class TogetherClient(OpenAICompatibleClient): 

141 """LLM client for Together AI (OpenAI-compatible). 

142 

143 Targets ``https://api.together.xyz/v1``. Authentication uses a Bearer 

144 token supplied as ``ClientConfig.api_key``. 

145 

146 Together AI hosts a large catalogue of open-source models; pass the full 

147 model path (e.g. ``meta-llama/Llama-3-8b-chat-hf``) as ``config.model``. 

148 

149 Example: 

150 >>> config = ClientConfig( 

151 ... provider="together", 

152 ... model="meta-llama/Llama-3-8b-chat-hf", 

153 ... api_key=SecretStr("tog-..."), 

154 ... ) 

155 >>> client = TogetherClient(config) 

156 """ 

157 

158 _provider_base_url = "https://api.together.xyz/v1" 

159 _provider_name = "together" 

160 

161 

162class FireworksClient(OpenAICompatibleClient): 

163 """LLM client for Fireworks AI (OpenAI-compatible). 

164 

165 Targets ``https://api.fireworks.ai/inference/v1``. Authentication uses a 

166 Bearer token supplied as ``ClientConfig.api_key``. 

167 

168 Pass model IDs in Fireworks format, e.g. 

169 ``accounts/fireworks/models/llama-v3-70b-instruct``. 

170 

171 Example: 

172 >>> config = ClientConfig( 

173 ... provider="fireworks", 

174 ... model="accounts/fireworks/models/llama-v3-70b-instruct", 

175 ... api_key=SecretStr("fw-..."), 

176 ... ) 

177 >>> client = FireworksClient(config) 

178 """ 

179 

180 _provider_base_url = "https://api.fireworks.ai/inference/v1" 

181 _provider_name = "fireworks"