generic-llm-api-client 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_client/__init__.py +66 -0
- ai_client/base_client.py +353 -0
- ai_client/claude_client.py +283 -0
- ai_client/deepseek_client.py +28 -0
- ai_client/gemini_client.py +269 -0
- ai_client/mistral_client.py +191 -0
- ai_client/openai_client.py +299 -0
- ai_client/qwen_client.py +28 -0
- ai_client/response.py +89 -0
- ai_client/utils.py +151 -0
- generic_llm_api_client-0.1.1.dist-info/METADATA +477 -0
- generic_llm_api_client-0.1.1.dist-info/RECORD +15 -0
- generic_llm_api_client-0.1.1.dist-info/WHEEL +5 -0
- generic_llm_api_client-0.1.1.dist-info/licenses/LICENSE +21 -0
- generic_llm_api_client-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeepSeek-specific client using OpenAI-compatible API.
|
|
3
|
+
|
|
4
|
+
DeepSeek uses an OpenAI-compatible API, so we can reuse the OpenAIClient
|
|
5
|
+
with a custom base URL.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .openai_client import OpenAIClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DeepSeekClient(OpenAIClient):
|
|
12
|
+
"""
|
|
13
|
+
DeepSeek client using OpenAI-compatible API.
|
|
14
|
+
|
|
15
|
+
This client simply extends OpenAIClient with a custom base URL.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
PROVIDER_ID = "deepseek"
|
|
19
|
+
SUPPORTS_MULTIMODAL = True
|
|
20
|
+
|
|
21
|
+
def _init_client(self):
|
|
22
|
+
"""Initialize the client with DeepSeek's base URL."""
|
|
23
|
+
# Override base_url if not provided
|
|
24
|
+
if not self.base_url:
|
|
25
|
+
self.base_url = "https://api.deepseek.com/v1"
|
|
26
|
+
|
|
27
|
+
# Call parent initialization
|
|
28
|
+
super()._init_client()
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Gemini-specific implementation of the BaseAIClient.
|
|
3
|
+
|
|
4
|
+
This module provides the GeminiClient class, which implements the BaseAIClient
|
|
5
|
+
interface specifically for Google's Gemini API, supporting both text-only and
|
|
6
|
+
multimodal (text + images) content.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from typing import List, Tuple, Any, Optional
|
|
13
|
+
|
|
14
|
+
import google.genai as genai
|
|
15
|
+
from google.genai.types import GenerateContentConfig, Part
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
from .base_client import BaseAIClient
|
|
19
|
+
from .response import LLMResponse, Usage
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GeminiClient(BaseAIClient):
|
|
25
|
+
"""
|
|
26
|
+
Google Gemini-specific implementation of the BaseAIClient.
|
|
27
|
+
|
|
28
|
+
This class implements the BaseAIClient interface for Google's Gemini API,
|
|
29
|
+
handling both text-only and multimodal (text + images) requests.
|
|
30
|
+
|
|
31
|
+
Key features:
|
|
32
|
+
- Full support for multimodal content via Gemini Pro Vision models
|
|
33
|
+
- Support for Gemini-specific parameters like top_k, top_p
|
|
34
|
+
- Structured output via response schema
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
PROVIDER_ID = "genai"
|
|
38
|
+
SUPPORTS_MULTIMODAL = True
|
|
39
|
+
|
|
40
|
+
def _init_client(self):
|
|
41
|
+
"""Initialize Gemini API client with the provided API key."""
|
|
42
|
+
self.api_client = genai.Client(api_key=self.api_key)
|
|
43
|
+
|
|
44
|
+
def _prepare_content_with_images(self, prompt: str, images: List[str]) -> List[Any]:
|
|
45
|
+
"""
|
|
46
|
+
Prepare Gemini content with text and images.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
prompt: The text prompt
|
|
50
|
+
images: List of image paths/URLs
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of content parts for Gemini API
|
|
54
|
+
"""
|
|
55
|
+
contents = [prompt]
|
|
56
|
+
|
|
57
|
+
# Add images if any
|
|
58
|
+
for resource in images:
|
|
59
|
+
try:
|
|
60
|
+
if self.is_url(resource):
|
|
61
|
+
# For URLs, fetch the image
|
|
62
|
+
response = requests.get(resource)
|
|
63
|
+
if response.status_code == 200:
|
|
64
|
+
image_data = response.content
|
|
65
|
+
else:
|
|
66
|
+
logger.error(
|
|
67
|
+
f"Failed to fetch image from URL {resource}: {response.status_code}"
|
|
68
|
+
)
|
|
69
|
+
continue
|
|
70
|
+
else:
|
|
71
|
+
# For local files, read the image
|
|
72
|
+
with open(resource, "rb") as f:
|
|
73
|
+
image_data = f.read()
|
|
74
|
+
|
|
75
|
+
# Detect MIME type from file extension
|
|
76
|
+
from .utils import detect_image_mime_type
|
|
77
|
+
|
|
78
|
+
mime_type = detect_image_mime_type(resource)
|
|
79
|
+
|
|
80
|
+
# Create image part
|
|
81
|
+
image_part = Part(
|
|
82
|
+
inline_data={
|
|
83
|
+
"mime_type": mime_type,
|
|
84
|
+
"data": base64.b64encode(image_data).decode("utf-8"),
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
contents.append(image_part)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Error processing image {resource}: {e}")
|
|
90
|
+
|
|
91
|
+
return contents
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _remove_defaults_from_schema(schema):
|
|
95
|
+
"""Recursively remove default values from JSON schema (for GenAI compatibility)."""
|
|
96
|
+
if isinstance(schema, dict):
|
|
97
|
+
if "default" in schema:
|
|
98
|
+
del schema["default"]
|
|
99
|
+
for key, value in schema.items():
|
|
100
|
+
if isinstance(value, (dict, list)):
|
|
101
|
+
GeminiClient._remove_defaults_from_schema(value)
|
|
102
|
+
elif isinstance(schema, list):
|
|
103
|
+
for item in schema:
|
|
104
|
+
if isinstance(item, (dict, list)):
|
|
105
|
+
GeminiClient._remove_defaults_from_schema(item)
|
|
106
|
+
|
|
107
|
+
def _do_prompt(
|
|
108
|
+
self,
|
|
109
|
+
model: str,
|
|
110
|
+
prompt: str,
|
|
111
|
+
images: List[str],
|
|
112
|
+
system_prompt: str,
|
|
113
|
+
response_format: Optional[Any],
|
|
114
|
+
**kwargs,
|
|
115
|
+
) -> LLMResponse:
|
|
116
|
+
"""
|
|
117
|
+
Send a prompt to the Gemini model and get the response.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
model: The Gemini model identifier (e.g., "gemini-pro", "gemini-pro-vision")
|
|
121
|
+
prompt: The text prompt to send
|
|
122
|
+
images: List of image paths/URLs
|
|
123
|
+
system_prompt: System prompt (prepended to prompt for Gemini)
|
|
124
|
+
response_format: Optional Pydantic model for structured output
|
|
125
|
+
**kwargs: Additional Gemini-specific parameters
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
LLMResponse object with the provider's response
|
|
129
|
+
"""
|
|
130
|
+
# Prepend system prompt to user prompt for Gemini
|
|
131
|
+
full_prompt = f"{system_prompt}\n\n{prompt}" if system_prompt else prompt
|
|
132
|
+
|
|
133
|
+
# Prepare content with images
|
|
134
|
+
contents = self._prepare_content_with_images(full_prompt, images)
|
|
135
|
+
|
|
136
|
+
# Build generation config
|
|
137
|
+
generation_config = {}
|
|
138
|
+
|
|
139
|
+
# Extract Gemini-specific parameters
|
|
140
|
+
optional_params = [
|
|
141
|
+
"temperature",
|
|
142
|
+
"top_p",
|
|
143
|
+
"top_k",
|
|
144
|
+
"max_output_tokens",
|
|
145
|
+
"candidate_count",
|
|
146
|
+
"stop_sequences",
|
|
147
|
+
]
|
|
148
|
+
for param in optional_params:
|
|
149
|
+
value = kwargs.get(param, self.settings.get(param))
|
|
150
|
+
if value is not None:
|
|
151
|
+
generation_config[param] = value
|
|
152
|
+
|
|
153
|
+
# Handle structured output
|
|
154
|
+
if response_format and hasattr(response_format, "model_json_schema"):
|
|
155
|
+
schema = response_format.model_json_schema()
|
|
156
|
+
# GenAI doesn't support default values in schema
|
|
157
|
+
self._remove_defaults_from_schema(schema)
|
|
158
|
+
|
|
159
|
+
generation_config["response_mime_type"] = "application/json"
|
|
160
|
+
generation_config["response_schema"] = schema
|
|
161
|
+
|
|
162
|
+
# Generate content
|
|
163
|
+
# Try different approaches based on what parameters are available
|
|
164
|
+
try:
|
|
165
|
+
if generation_config:
|
|
166
|
+
# Try passing config as a GenerateContentConfig object
|
|
167
|
+
config = GenerateContentConfig(**generation_config)
|
|
168
|
+
raw_response = self.api_client.models.generate_content(
|
|
169
|
+
model=model, contents=contents, config=config
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
raw_response = self.api_client.models.generate_content(
|
|
173
|
+
model=model, contents=contents
|
|
174
|
+
)
|
|
175
|
+
except TypeError as e:
|
|
176
|
+
# If config parameter doesn't work, try passing generation_config directly
|
|
177
|
+
if "config" in str(e):
|
|
178
|
+
logger.warning(f"Config parameter not accepted, trying alternative API: {e}")
|
|
179
|
+
if generation_config:
|
|
180
|
+
raw_response = self.api_client.models.generate_content(
|
|
181
|
+
model=model, contents=contents, generation_config=generation_config
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
raw_response = self.api_client.models.generate_content(
|
|
185
|
+
model=model, contents=contents
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
return self._create_response_from_raw(raw_response, model, response_format)
|
|
191
|
+
|
|
192
|
+
def _create_response_from_raw(
|
|
193
|
+
self, raw_response: Any, model: str, response_format: Optional[Any]
|
|
194
|
+
) -> LLMResponse:
|
|
195
|
+
"""
|
|
196
|
+
Create LLMResponse from Gemini raw response.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
raw_response: Raw Gemini response object
|
|
200
|
+
model: Model identifier
|
|
201
|
+
response_format: Pydantic model if structured output was requested
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
LLMResponse object
|
|
205
|
+
"""
|
|
206
|
+
text = raw_response.text if hasattr(raw_response, "text") else ""
|
|
207
|
+
|
|
208
|
+
# If response_format was provided, try to validate
|
|
209
|
+
if response_format and hasattr(response_format, "model_json_schema") and text:
|
|
210
|
+
try:
|
|
211
|
+
json_data = json.loads(text)
|
|
212
|
+
validated = response_format(**json_data)
|
|
213
|
+
text = validated.model_dump_json()
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.warning(f"Failed to validate Gemini structured response: {e}")
|
|
216
|
+
# Keep original text
|
|
217
|
+
|
|
218
|
+
usage = Usage()
|
|
219
|
+
if hasattr(raw_response, "usage_metadata"):
|
|
220
|
+
usage = Usage(
|
|
221
|
+
input_tokens=raw_response.usage_metadata.prompt_token_count,
|
|
222
|
+
output_tokens=raw_response.usage_metadata.candidates_token_count,
|
|
223
|
+
total_tokens=raw_response.usage_metadata.total_token_count,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Determine finish reason
|
|
227
|
+
finish_reason = "unknown"
|
|
228
|
+
if hasattr(raw_response, "candidates") and raw_response.candidates:
|
|
229
|
+
candidate = raw_response.candidates[0]
|
|
230
|
+
if hasattr(candidate, "finish_reason"):
|
|
231
|
+
# Extract the value from the enum (e.g., FinishReason.STOP -> 'STOP')
|
|
232
|
+
fr = candidate.finish_reason
|
|
233
|
+
if hasattr(fr, "value"):
|
|
234
|
+
finish_reason = fr.value
|
|
235
|
+
elif hasattr(fr, "name"):
|
|
236
|
+
finish_reason = fr.name
|
|
237
|
+
else:
|
|
238
|
+
finish_reason = str(fr)
|
|
239
|
+
|
|
240
|
+
return LLMResponse(
|
|
241
|
+
text=text,
|
|
242
|
+
model=model,
|
|
243
|
+
provider=self.PROVIDER_ID,
|
|
244
|
+
finish_reason=finish_reason,
|
|
245
|
+
usage=usage,
|
|
246
|
+
raw_response=raw_response,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def get_model_list(self) -> List[Tuple[str, Optional[str]]]:
|
|
250
|
+
"""
|
|
251
|
+
Get a list of available models from Gemini.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
List of tuples (model_id, created_date)
|
|
255
|
+
"""
|
|
256
|
+
model_list = []
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
raw_list = self.api_client.models.list()
|
|
260
|
+
for model in raw_list:
|
|
261
|
+
model_name = model.name if hasattr(model, "name") else str(model)
|
|
262
|
+
# Remove 'models/' prefix if present
|
|
263
|
+
if model_name.startswith("models/"):
|
|
264
|
+
model_name = model_name[7:]
|
|
265
|
+
model_list.append((model_name, None))
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.error(f"Error listing Gemini models: {e}")
|
|
268
|
+
|
|
269
|
+
return model_list
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mistral-specific implementation of the BaseAIClient.
|
|
3
|
+
|
|
4
|
+
This module provides the MistralClient class, which implements the BaseAIClient
|
|
5
|
+
interface specifically for Mistral's API, supporting both text-only and multimodal
|
|
6
|
+
interactions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from typing import List, Tuple, Any, Optional
|
|
13
|
+
|
|
14
|
+
from mistralai import Mistral
|
|
15
|
+
|
|
16
|
+
from .base_client import BaseAIClient
|
|
17
|
+
from .response import LLMResponse, Usage
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MistralClient(BaseAIClient):
|
|
23
|
+
"""
|
|
24
|
+
Mistral-specific implementation of the BaseAIClient.
|
|
25
|
+
|
|
26
|
+
This class implements the BaseAIClient interface for Mistral's API,
|
|
27
|
+
supporting text-only and multimodal requests via the chat completion API.
|
|
28
|
+
|
|
29
|
+
Key features:
|
|
30
|
+
- Integration with Mistral's chat completion API
|
|
31
|
+
- Support for multimodal content (text + images)
|
|
32
|
+
- Support for Mistral-specific parameters
|
|
33
|
+
- Structured output via JSON mode
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
PROVIDER_ID = "mistral"
|
|
37
|
+
SUPPORTS_MULTIMODAL = True # Mistral supports images
|
|
38
|
+
|
|
39
|
+
def _init_client(self):
|
|
40
|
+
"""Initialize the Mistral client with the provided API key."""
|
|
41
|
+
self.api_client = Mistral(api_key=self.api_key)
|
|
42
|
+
|
|
43
|
+
def _prepare_content_with_images(self, prompt: str, images: List[str]) -> List[dict]:
|
|
44
|
+
"""
|
|
45
|
+
Prepare Mistral content with text and images.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
prompt: The text prompt
|
|
49
|
+
images: List of image paths/URLs
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of content blocks for Mistral API
|
|
53
|
+
"""
|
|
54
|
+
content = [{"type": "text", "text": prompt}]
|
|
55
|
+
|
|
56
|
+
# Add images if any
|
|
57
|
+
for resource in images:
|
|
58
|
+
try:
|
|
59
|
+
if self.is_url(resource):
|
|
60
|
+
# For URLs, use directly
|
|
61
|
+
data_uri = resource
|
|
62
|
+
else:
|
|
63
|
+
# For local files, encode as base64
|
|
64
|
+
with open(resource, "rb") as image_file:
|
|
65
|
+
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
|
|
66
|
+
|
|
67
|
+
# Detect MIME type from file extension
|
|
68
|
+
from .utils import detect_image_mime_type
|
|
69
|
+
|
|
70
|
+
mime_type = detect_image_mime_type(resource)
|
|
71
|
+
|
|
72
|
+
data_uri = f"data:{mime_type};base64,{base64_image}"
|
|
73
|
+
|
|
74
|
+
content.append({"type": "image_url", "image_url": {"url": data_uri}})
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Error processing image {resource}: {e}")
|
|
77
|
+
|
|
78
|
+
return content
|
|
79
|
+
|
|
80
|
+
def _do_prompt(
|
|
81
|
+
self,
|
|
82
|
+
model: str,
|
|
83
|
+
prompt: str,
|
|
84
|
+
images: List[str],
|
|
85
|
+
system_prompt: str,
|
|
86
|
+
response_format: Optional[Any],
|
|
87
|
+
**kwargs,
|
|
88
|
+
) -> LLMResponse:
|
|
89
|
+
"""
|
|
90
|
+
Send a prompt to the Mistral model and get the response.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
model: The Mistral model identifier
|
|
94
|
+
prompt: The text prompt to send
|
|
95
|
+
images: List of image paths/URLs
|
|
96
|
+
system_prompt: System prompt to use
|
|
97
|
+
response_format: Optional Pydantic model for structured output
|
|
98
|
+
**kwargs: Additional Mistral-specific parameters
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
LLMResponse object with the provider's response
|
|
102
|
+
"""
|
|
103
|
+
# Prepare content with images
|
|
104
|
+
content = self._prepare_content_with_images(prompt, images)
|
|
105
|
+
|
|
106
|
+
# Build messages
|
|
107
|
+
messages = [{"role": "user", "content": content}]
|
|
108
|
+
|
|
109
|
+
# Add system message if provided
|
|
110
|
+
if system_prompt:
|
|
111
|
+
messages.insert(0, {"role": "system", "content": system_prompt})
|
|
112
|
+
|
|
113
|
+
params = {"messages": messages, "model": model}
|
|
114
|
+
|
|
115
|
+
# Handle structured output
|
|
116
|
+
if response_format and hasattr(response_format, "model_json_schema"):
|
|
117
|
+
schema = response_format.model_json_schema()
|
|
118
|
+
schema_prompt = (
|
|
119
|
+
f"\n\nReturn a JSON response matching this exact schema: {json.dumps(schema)}"
|
|
120
|
+
)
|
|
121
|
+
messages[-1]["content"] = prompt + schema_prompt
|
|
122
|
+
params["response_format"] = {"type": "json_object"}
|
|
123
|
+
|
|
124
|
+
# Send the request
|
|
125
|
+
raw_response = self.api_client.chat.complete(**params)
|
|
126
|
+
|
|
127
|
+
return self._create_response_from_raw(raw_response, model, response_format)
|
|
128
|
+
|
|
129
|
+
def _create_response_from_raw(
|
|
130
|
+
self, raw_response: Any, model: str, response_format: Optional[Any]
|
|
131
|
+
) -> LLMResponse:
|
|
132
|
+
"""
|
|
133
|
+
Create LLMResponse from Mistral raw response.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
raw_response: Raw Mistral response object
|
|
137
|
+
model: Model identifier
|
|
138
|
+
response_format: Pydantic model if structured output was requested
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
LLMResponse object
|
|
142
|
+
"""
|
|
143
|
+
choice = raw_response.choices[0]
|
|
144
|
+
text = choice.message.content if hasattr(choice.message, "content") else ""
|
|
145
|
+
|
|
146
|
+
# If response_format was provided, try to validate
|
|
147
|
+
if response_format and hasattr(response_format, "model_json_schema") and text:
|
|
148
|
+
try:
|
|
149
|
+
json_data = json.loads(text)
|
|
150
|
+
validated = response_format(**json_data)
|
|
151
|
+
text = validated.model_dump_json()
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.warning(f"Failed to validate Mistral structured response: {e}")
|
|
154
|
+
# Keep original text
|
|
155
|
+
|
|
156
|
+
usage = Usage()
|
|
157
|
+
if hasattr(raw_response, "usage") and raw_response.usage:
|
|
158
|
+
usage = Usage(
|
|
159
|
+
input_tokens=raw_response.usage.prompt_tokens,
|
|
160
|
+
output_tokens=raw_response.usage.completion_tokens,
|
|
161
|
+
total_tokens=raw_response.usage.total_tokens,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
finish_reason = choice.finish_reason if hasattr(choice, "finish_reason") else "unknown"
|
|
165
|
+
|
|
166
|
+
return LLMResponse(
|
|
167
|
+
text=text,
|
|
168
|
+
model=model,
|
|
169
|
+
provider=self.PROVIDER_ID,
|
|
170
|
+
finish_reason=finish_reason,
|
|
171
|
+
usage=usage,
|
|
172
|
+
raw_response=raw_response,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def get_model_list(self) -> List[Tuple[str, Optional[str]]]:
|
|
176
|
+
"""
|
|
177
|
+
Get a list of available models from Mistral.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of tuples (model_id, created_date)
|
|
181
|
+
"""
|
|
182
|
+
if self.api_client is None:
|
|
183
|
+
raise ValueError("Mistral client is not initialized.")
|
|
184
|
+
|
|
185
|
+
model_list = []
|
|
186
|
+
raw_list = self.api_client.models.list()
|
|
187
|
+
|
|
188
|
+
for model in raw_list:
|
|
189
|
+
model_list.append((model.id, None))
|
|
190
|
+
|
|
191
|
+
return model_list
|