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"