ccs-llmconnector 1.0.6__py3-none-any.whl → 1.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.
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/METADATA +51 -8
- ccs_llmconnector-1.1.1.dist-info/RECORD +16 -0
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/WHEEL +1 -1
- llmconnector/__init__.py +11 -1
- llmconnector/anthropic_client.py +209 -66
- llmconnector/client.py +225 -10
- llmconnector/client_cli.py +27 -0
- llmconnector/gemini_client.py +316 -119
- llmconnector/grok_client.py +208 -78
- llmconnector/openai_client.py +194 -62
- llmconnector/types.py +49 -0
- llmconnector/utils.py +78 -0
- ccs_llmconnector-1.0.6.dist-info/RECORD +0 -14
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/entry_points.txt +0 -0
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/top_level.txt +0 -0
llmconnector/grok_client.py
CHANGED
|
@@ -6,13 +6,15 @@ import base64
|
|
|
6
6
|
import mimetypes
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Optional, Sequence
|
|
9
|
+
from typing import Optional, Sequence
|
|
10
10
|
|
|
11
11
|
from xai_sdk import Client
|
|
12
12
|
from xai_sdk.chat import image as chat_image
|
|
13
13
|
from xai_sdk.chat import user
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
from .types import ImageInput, MessageSequence, normalize_messages
|
|
16
|
+
from .utils import clamp_retries, run_sync_in_thread, run_with_retries
|
|
17
|
+
|
|
16
18
|
logger = logging.getLogger(__name__)
|
|
17
19
|
|
|
18
20
|
|
|
@@ -23,11 +25,16 @@ class GrokClient:
|
|
|
23
25
|
self,
|
|
24
26
|
*,
|
|
25
27
|
api_key: str,
|
|
26
|
-
prompt: str,
|
|
28
|
+
prompt: Optional[str] = None,
|
|
27
29
|
model: str,
|
|
28
30
|
max_tokens: int = 32000,
|
|
29
31
|
reasoning_effort: Optional[str] = None,
|
|
30
32
|
images: Optional[Sequence[ImageInput]] = None,
|
|
33
|
+
messages: Optional[MessageSequence] = None,
|
|
34
|
+
request_id: Optional[str] = None,
|
|
35
|
+
timeout_s: Optional[float] = None,
|
|
36
|
+
max_retries: Optional[int] = None,
|
|
37
|
+
retry_backoff_s: float = 0.5,
|
|
31
38
|
) -> str:
|
|
32
39
|
"""Generate a response from the specified Grok model.
|
|
33
40
|
|
|
@@ -38,6 +45,11 @@ class GrokClient:
|
|
|
38
45
|
max_tokens: Cap for tokens in the generated response, defaults to 32000.
|
|
39
46
|
reasoning_effort: Optional hint for reasoning-focused models (``"low"`` or ``"high"``).
|
|
40
47
|
images: Optional collection of image references (local paths, URLs, or data URLs).
|
|
48
|
+
messages: Optional list of chat-style messages (role/content).
|
|
49
|
+
request_id: Optional request identifier for tracing/logging.
|
|
50
|
+
timeout_s: Optional request timeout in seconds (best-effort).
|
|
51
|
+
max_retries: Optional retry count for transient failures.
|
|
52
|
+
retry_backoff_s: Base delay (seconds) for exponential backoff between retries.
|
|
41
53
|
|
|
42
54
|
Returns:
|
|
43
55
|
The text output produced by the model.
|
|
@@ -48,14 +60,17 @@ class GrokClient:
|
|
|
48
60
|
"""
|
|
49
61
|
if not api_key:
|
|
50
62
|
raise ValueError("api_key must be provided.")
|
|
51
|
-
if not prompt and not images:
|
|
52
|
-
raise ValueError("At least one of prompt or images must be provided.")
|
|
63
|
+
if not prompt and not messages and not images:
|
|
64
|
+
raise ValueError("At least one of prompt, messages, or images must be provided.")
|
|
53
65
|
if not model:
|
|
54
66
|
raise ValueError("model must be provided.")
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
normalized_messages = normalize_messages(prompt=prompt, messages=messages)
|
|
69
|
+
message_parts: list[object] = []
|
|
70
|
+
if normalized_messages:
|
|
71
|
+
for message in normalized_messages:
|
|
72
|
+
if message["content"]:
|
|
73
|
+
message_parts.append(f"{message['role']}: {message['content']}")
|
|
59
74
|
|
|
60
75
|
if images:
|
|
61
76
|
for image in images:
|
|
@@ -64,53 +79,104 @@ class GrokClient:
|
|
|
64
79
|
if not message_parts:
|
|
65
80
|
raise ValueError("No content provided for response generation.")
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
retry_count = clamp_retries(max_retries)
|
|
83
|
+
|
|
84
|
+
def _run_request() -> str:
|
|
85
|
+
grok_client = Client(api_key=api_key)
|
|
86
|
+
|
|
87
|
+
create_kwargs = {
|
|
88
|
+
"model": model,
|
|
89
|
+
"max_tokens": max_tokens,
|
|
90
|
+
"messages": [user(*message_parts)],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
normalized_effort = (reasoning_effort or "").strip().lower()
|
|
94
|
+
if normalized_effort in {"low", "high"}:
|
|
95
|
+
create_kwargs["reasoning_effort"] = normalized_effort
|
|
96
|
+
|
|
97
|
+
if timeout_s is not None:
|
|
98
|
+
create_kwargs["timeout"] = timeout_s
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
chat = grok_client.chat.create(**create_kwargs)
|
|
102
|
+
response = chat.sample()
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
logger.exception(
|
|
105
|
+
"xAI Grok chat request failed: %s request_id=%s",
|
|
106
|
+
exc,
|
|
107
|
+
request_id,
|
|
108
|
+
)
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
content = getattr(response, "content", None)
|
|
112
|
+
if content:
|
|
113
|
+
logger.info(
|
|
114
|
+
"xAI chat succeeded: model=%s images=%d text_len=%d request_id=%s",
|
|
115
|
+
model,
|
|
116
|
+
len(images or []),
|
|
117
|
+
len(content or ""),
|
|
118
|
+
request_id,
|
|
119
|
+
)
|
|
120
|
+
return content
|
|
121
|
+
|
|
122
|
+
reasoning_content = getattr(response, "reasoning_content", None)
|
|
123
|
+
if reasoning_content:
|
|
124
|
+
logger.info(
|
|
125
|
+
"xAI chat succeeded (reasoning): model=%s images=%d text_len=%d request_id=%s",
|
|
126
|
+
model,
|
|
127
|
+
len(images or []),
|
|
128
|
+
len(reasoning_content or ""),
|
|
129
|
+
request_id,
|
|
130
|
+
)
|
|
131
|
+
return reasoning_content
|
|
132
|
+
|
|
133
|
+
# Treat successful calls without textual content as a successful, empty response
|
|
134
|
+
# rather than raising. This aligns with callers that handle empty outputs gracefully.
|
|
88
135
|
logger.info(
|
|
89
|
-
"xAI chat succeeded: model=%s images=%d
|
|
136
|
+
"xAI chat succeeded with no text: model=%s images=%d request_id=%s",
|
|
90
137
|
model,
|
|
91
138
|
len(images or []),
|
|
92
|
-
|
|
139
|
+
request_id,
|
|
93
140
|
)
|
|
94
|
-
return
|
|
141
|
+
return ""
|
|
95
142
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
143
|
+
return run_with_retries(
|
|
144
|
+
func=_run_request,
|
|
145
|
+
max_retries=retry_count,
|
|
146
|
+
retry_backoff_s=retry_backoff_s,
|
|
147
|
+
request_id=request_id,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def async_generate_response(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
api_key: str,
|
|
154
|
+
prompt: Optional[str] = None,
|
|
155
|
+
model: str,
|
|
156
|
+
max_tokens: int = 32000,
|
|
157
|
+
reasoning_effort: Optional[str] = None,
|
|
158
|
+
images: Optional[Sequence[ImageInput]] = None,
|
|
159
|
+
messages: Optional[MessageSequence] = None,
|
|
160
|
+
request_id: Optional[str] = None,
|
|
161
|
+
timeout_s: Optional[float] = None,
|
|
162
|
+
max_retries: Optional[int] = None,
|
|
163
|
+
retry_backoff_s: float = 0.5,
|
|
164
|
+
) -> str:
|
|
165
|
+
return await run_sync_in_thread(
|
|
166
|
+
lambda: self.generate_response(
|
|
167
|
+
api_key=api_key,
|
|
168
|
+
prompt=prompt,
|
|
169
|
+
model=model,
|
|
170
|
+
max_tokens=max_tokens,
|
|
171
|
+
reasoning_effort=reasoning_effort,
|
|
172
|
+
images=images,
|
|
173
|
+
messages=messages,
|
|
174
|
+
request_id=request_id,
|
|
175
|
+
timeout_s=timeout_s,
|
|
176
|
+
max_retries=max_retries,
|
|
177
|
+
retry_backoff_s=retry_backoff_s,
|
|
103
178
|
)
|
|
104
|
-
return reasoning_content
|
|
105
|
-
|
|
106
|
-
# Treat successful calls without textual content as a successful, empty response
|
|
107
|
-
# rather than raising. This aligns with callers that handle empty outputs gracefully.
|
|
108
|
-
logger.info(
|
|
109
|
-
"xAI chat succeeded with no text: model=%s images=%d",
|
|
110
|
-
model,
|
|
111
|
-
len(images or []),
|
|
112
179
|
)
|
|
113
|
-
return ""
|
|
114
180
|
|
|
115
181
|
def generate_image(
|
|
116
182
|
self,
|
|
@@ -128,42 +194,106 @@ class GrokClient:
|
|
|
128
194
|
"""
|
|
129
195
|
raise NotImplementedError("Image generation is not implemented for Grok.")
|
|
130
196
|
|
|
131
|
-
def
|
|
197
|
+
async def async_generate_image(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
api_key: str,
|
|
201
|
+
prompt: str,
|
|
202
|
+
model: str,
|
|
203
|
+
image_size: str = "2K",
|
|
204
|
+
image: Optional[ImageInput] = None,
|
|
205
|
+
) -> bytes:
|
|
206
|
+
return await run_sync_in_thread(
|
|
207
|
+
lambda: self.generate_image(
|
|
208
|
+
api_key=api_key,
|
|
209
|
+
prompt=prompt,
|
|
210
|
+
model=model,
|
|
211
|
+
image_size=image_size,
|
|
212
|
+
image=image,
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def list_models(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
api_key: str,
|
|
220
|
+
request_id: Optional[str] = None,
|
|
221
|
+
timeout_s: Optional[float] = None,
|
|
222
|
+
max_retries: Optional[int] = None,
|
|
223
|
+
retry_backoff_s: float = 0.5,
|
|
224
|
+
) -> list[dict[str, Optional[str]]]:
|
|
132
225
|
"""Return the Grok language models available to the authenticated account."""
|
|
133
226
|
if not api_key:
|
|
134
227
|
raise ValueError("api_key must be provided.")
|
|
135
228
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
229
|
+
retry_count = clamp_retries(max_retries)
|
|
230
|
+
|
|
231
|
+
def _run_request() -> list[dict[str, Optional[str]]]:
|
|
232
|
+
grok_client = Client(api_key=api_key)
|
|
233
|
+
models: list[dict[str, Optional[str]]] = []
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
iterator = grok_client.models.list_language_models()
|
|
237
|
+
except Exception as exc:
|
|
238
|
+
logger.exception(
|
|
239
|
+
"xAI list language models failed: %s request_id=%s",
|
|
240
|
+
exc,
|
|
241
|
+
request_id,
|
|
242
|
+
)
|
|
243
|
+
raise
|
|
244
|
+
|
|
245
|
+
for model in iterator:
|
|
246
|
+
model_id = getattr(model, "name", None)
|
|
247
|
+
if not model_id:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
aliases = getattr(model, "aliases", None)
|
|
251
|
+
display_name: Optional[str] = None
|
|
252
|
+
if aliases:
|
|
253
|
+
try:
|
|
254
|
+
display_name = next(iter(aliases)) or None
|
|
255
|
+
except (StopIteration, TypeError):
|
|
256
|
+
display_name = None
|
|
257
|
+
|
|
258
|
+
models.append(
|
|
259
|
+
{
|
|
260
|
+
"id": model_id,
|
|
261
|
+
"display_name": display_name,
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
logger.info(
|
|
266
|
+
"xAI list_language_models succeeded: count=%d request_id=%s",
|
|
267
|
+
len(models),
|
|
268
|
+
request_id,
|
|
163
269
|
)
|
|
270
|
+
return models
|
|
271
|
+
|
|
272
|
+
return run_with_retries(
|
|
273
|
+
func=_run_request,
|
|
274
|
+
max_retries=retry_count,
|
|
275
|
+
retry_backoff_s=retry_backoff_s,
|
|
276
|
+
request_id=request_id,
|
|
277
|
+
)
|
|
164
278
|
|
|
165
|
-
|
|
166
|
-
|
|
279
|
+
async def async_list_models(
|
|
280
|
+
self,
|
|
281
|
+
*,
|
|
282
|
+
api_key: str,
|
|
283
|
+
request_id: Optional[str] = None,
|
|
284
|
+
timeout_s: Optional[float] = None,
|
|
285
|
+
max_retries: Optional[int] = None,
|
|
286
|
+
retry_backoff_s: float = 0.5,
|
|
287
|
+
) -> list[dict[str, Optional[str]]]:
|
|
288
|
+
return await run_sync_in_thread(
|
|
289
|
+
lambda: self.list_models(
|
|
290
|
+
api_key=api_key,
|
|
291
|
+
request_id=request_id,
|
|
292
|
+
timeout_s=timeout_s,
|
|
293
|
+
max_retries=max_retries,
|
|
294
|
+
retry_backoff_s=retry_backoff_s,
|
|
295
|
+
)
|
|
296
|
+
)
|
|
167
297
|
|
|
168
298
|
@staticmethod
|
|
169
299
|
def _to_image_url(image: ImageInput) -> str:
|
llmconnector/openai_client.py
CHANGED
|
@@ -6,16 +6,13 @@ import base64
|
|
|
6
6
|
import mimetypes
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Optional, Sequence
|
|
9
|
+
from typing import Optional, Sequence
|
|
10
10
|
|
|
11
11
|
from openai import OpenAI
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
except ImportError: # pragma: no cover - fallback for older SDKs
|
|
16
|
-
from openai.error import OpenAIError # type: ignore
|
|
13
|
+
from .types import ImageInput, MessageSequence, normalize_messages
|
|
14
|
+
from .utils import clamp_retries, run_sync_in_thread, run_with_retries
|
|
17
15
|
|
|
18
|
-
ImageInput = Union[str, Path]
|
|
19
16
|
logger = logging.getLogger(__name__)
|
|
20
17
|
|
|
21
18
|
|
|
@@ -26,11 +23,16 @@ class OpenAIResponsesClient:
|
|
|
26
23
|
self,
|
|
27
24
|
*,
|
|
28
25
|
api_key: str,
|
|
29
|
-
prompt: str,
|
|
26
|
+
prompt: Optional[str] = None,
|
|
30
27
|
model: str,
|
|
31
28
|
max_tokens: int = 32000,
|
|
32
29
|
reasoning_effort: Optional[str] = None,
|
|
33
30
|
images: Optional[Sequence[ImageInput]] = None,
|
|
31
|
+
messages: Optional[MessageSequence] = None,
|
|
32
|
+
request_id: Optional[str] = None,
|
|
33
|
+
timeout_s: Optional[float] = None,
|
|
34
|
+
max_retries: Optional[int] = None,
|
|
35
|
+
retry_backoff_s: float = 0.5,
|
|
34
36
|
) -> str:
|
|
35
37
|
"""Generate a response from the specified model.
|
|
36
38
|
|
|
@@ -41,6 +43,11 @@ class OpenAIResponsesClient:
|
|
|
41
43
|
max_tokens: Cap for tokens across the entire exchange, defaults to 32000.
|
|
42
44
|
reasoning_effort: Optional reasoning effort hint (``"low"``, ``"medium"``, or ``"high"``).
|
|
43
45
|
images: Optional collection of image references (local paths or URLs).
|
|
46
|
+
messages: Optional list of chat-style messages (role/content).
|
|
47
|
+
request_id: Optional request identifier for tracing/logging.
|
|
48
|
+
timeout_s: Optional request timeout in seconds.
|
|
49
|
+
max_retries: Optional retry count for transient failures.
|
|
50
|
+
retry_backoff_s: Base delay (seconds) for exponential backoff between retries.
|
|
44
51
|
|
|
45
52
|
Returns:
|
|
46
53
|
The text output produced by the model.
|
|
@@ -51,52 +58,110 @@ class OpenAIResponsesClient:
|
|
|
51
58
|
"""
|
|
52
59
|
if not api_key:
|
|
53
60
|
raise ValueError("api_key must be provided.")
|
|
54
|
-
if not prompt and not images:
|
|
55
|
-
raise ValueError("At least one of prompt or images must be provided.")
|
|
61
|
+
if not prompt and not messages and not images:
|
|
62
|
+
raise ValueError("At least one of prompt, messages, or images must be provided.")
|
|
56
63
|
if not model:
|
|
57
64
|
raise ValueError("model must be provided.")
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
normalized_messages = normalize_messages(prompt=prompt, messages=messages)
|
|
67
|
+
content_messages: list[dict] = []
|
|
68
|
+
for message in normalized_messages:
|
|
69
|
+
content_blocks = []
|
|
70
|
+
if message["content"]:
|
|
71
|
+
content_blocks.append({"type": "input_text", "text": message["content"]})
|
|
72
|
+
content_messages.append({"role": message["role"], "content": content_blocks})
|
|
64
73
|
|
|
65
74
|
if images:
|
|
66
|
-
for image in images
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
image_blocks = [self._to_image_block(image) for image in images]
|
|
76
|
+
target_index = next(
|
|
77
|
+
(
|
|
78
|
+
index
|
|
79
|
+
for index in range(len(content_messages) - 1, -1, -1)
|
|
80
|
+
if content_messages[index]["role"] == "user"
|
|
81
|
+
),
|
|
82
|
+
None,
|
|
83
|
+
)
|
|
84
|
+
if target_index is None:
|
|
85
|
+
content_messages.append({"role": "user", "content": image_blocks})
|
|
86
|
+
else:
|
|
87
|
+
content_messages[target_index]["content"].extend(image_blocks)
|
|
88
|
+
|
|
89
|
+
if not any(message["content"] for message in content_messages):
|
|
70
90
|
raise ValueError("No content provided for response generation.")
|
|
71
91
|
|
|
72
92
|
request_payload = {
|
|
73
93
|
"model": model,
|
|
74
|
-
"input":
|
|
75
|
-
{
|
|
76
|
-
"role": "user",
|
|
77
|
-
"content": content_blocks,
|
|
78
|
-
}
|
|
79
|
-
],
|
|
94
|
+
"input": content_messages,
|
|
80
95
|
"max_output_tokens": max_tokens,
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
if reasoning_effort:
|
|
84
99
|
request_payload["reasoning"] = {"effort": reasoning_effort}
|
|
85
100
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
retry_count = clamp_retries(max_retries)
|
|
102
|
+
|
|
103
|
+
def _run_request() -> str:
|
|
104
|
+
client_kwargs = {"api_key": api_key}
|
|
105
|
+
if timeout_s is not None:
|
|
106
|
+
client_kwargs["timeout"] = timeout_s
|
|
107
|
+
client = OpenAI(**client_kwargs)
|
|
108
|
+
try:
|
|
109
|
+
response = client.responses.create(**request_payload)
|
|
110
|
+
except Exception as exc: # Log and re-raise to preserve default behavior
|
|
111
|
+
logger.exception(
|
|
112
|
+
"OpenAI Responses API request failed: %s request_id=%s",
|
|
113
|
+
exc,
|
|
114
|
+
request_id,
|
|
115
|
+
)
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
output_text = response.output_text
|
|
119
|
+
logger.info(
|
|
120
|
+
"OpenAI generate_response succeeded: model=%s images=%d text_len=%d request_id=%s",
|
|
121
|
+
model,
|
|
122
|
+
len(images or []),
|
|
123
|
+
len(output_text or ""),
|
|
124
|
+
request_id,
|
|
125
|
+
)
|
|
126
|
+
return output_text
|
|
127
|
+
|
|
128
|
+
return run_with_retries(
|
|
129
|
+
func=_run_request,
|
|
130
|
+
max_retries=retry_count,
|
|
131
|
+
retry_backoff_s=retry_backoff_s,
|
|
132
|
+
request_id=request_id,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def async_generate_response(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
api_key: str,
|
|
139
|
+
prompt: Optional[str] = None,
|
|
140
|
+
model: str,
|
|
141
|
+
max_tokens: int = 32000,
|
|
142
|
+
reasoning_effort: Optional[str] = None,
|
|
143
|
+
images: Optional[Sequence[ImageInput]] = None,
|
|
144
|
+
messages: Optional[MessageSequence] = None,
|
|
145
|
+
request_id: Optional[str] = None,
|
|
146
|
+
timeout_s: Optional[float] = None,
|
|
147
|
+
max_retries: Optional[int] = None,
|
|
148
|
+
retry_backoff_s: float = 0.5,
|
|
149
|
+
) -> str:
|
|
150
|
+
return await run_sync_in_thread(
|
|
151
|
+
lambda: self.generate_response(
|
|
152
|
+
api_key=api_key,
|
|
153
|
+
prompt=prompt,
|
|
154
|
+
model=model,
|
|
155
|
+
max_tokens=max_tokens,
|
|
156
|
+
reasoning_effort=reasoning_effort,
|
|
157
|
+
images=images,
|
|
158
|
+
messages=messages,
|
|
159
|
+
request_id=request_id,
|
|
160
|
+
timeout_s=timeout_s,
|
|
161
|
+
max_retries=max_retries,
|
|
162
|
+
retry_backoff_s=retry_backoff_s,
|
|
163
|
+
)
|
|
98
164
|
)
|
|
99
|
-
return output_text
|
|
100
165
|
|
|
101
166
|
@staticmethod
|
|
102
167
|
def _to_image_block(image: ImageInput) -> dict:
|
|
@@ -135,35 +200,102 @@ class OpenAIResponsesClient:
|
|
|
135
200
|
"""
|
|
136
201
|
raise NotImplementedError("Image generation is not implemented for OpenAI.")
|
|
137
202
|
|
|
138
|
-
def
|
|
203
|
+
async def async_generate_image(
|
|
204
|
+
self,
|
|
205
|
+
*,
|
|
206
|
+
api_key: str,
|
|
207
|
+
prompt: str,
|
|
208
|
+
model: str,
|
|
209
|
+
image_size: Optional[str] = None,
|
|
210
|
+
aspect_ratio: Optional[str] = None,
|
|
211
|
+
image: Optional[ImageInput] = None,
|
|
212
|
+
) -> bytes:
|
|
213
|
+
return await run_sync_in_thread(
|
|
214
|
+
lambda: self.generate_image(
|
|
215
|
+
api_key=api_key,
|
|
216
|
+
prompt=prompt,
|
|
217
|
+
model=model,
|
|
218
|
+
image_size=image_size,
|
|
219
|
+
aspect_ratio=aspect_ratio,
|
|
220
|
+
image=image,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def list_models(
|
|
225
|
+
self,
|
|
226
|
+
*,
|
|
227
|
+
api_key: str,
|
|
228
|
+
request_id: Optional[str] = None,
|
|
229
|
+
timeout_s: Optional[float] = None,
|
|
230
|
+
max_retries: Optional[int] = None,
|
|
231
|
+
retry_backoff_s: float = 0.5,
|
|
232
|
+
) -> list[dict[str, Optional[str]]]:
|
|
139
233
|
"""Return the models available to the authenticated OpenAI account."""
|
|
140
234
|
if not api_key:
|
|
141
235
|
raise ValueError("api_key must be provided.")
|
|
142
236
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
237
|
+
retry_count = clamp_retries(max_retries)
|
|
238
|
+
|
|
239
|
+
def _run_request() -> list[dict[str, Optional[str]]]:
|
|
240
|
+
client_kwargs = {"api_key": api_key}
|
|
241
|
+
if timeout_s is not None:
|
|
242
|
+
client_kwargs["timeout"] = timeout_s
|
|
243
|
+
client = OpenAI(**client_kwargs)
|
|
244
|
+
try:
|
|
245
|
+
response = client.models.list()
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
logger.exception(
|
|
248
|
+
"OpenAI list models failed: %s request_id=%s", exc, request_id
|
|
249
|
+
)
|
|
250
|
+
raise
|
|
251
|
+
data = getattr(response, "data", []) or []
|
|
252
|
+
|
|
253
|
+
models: list[dict[str, Optional[str]]] = []
|
|
254
|
+
for model in data:
|
|
255
|
+
model_id = getattr(model, "id", None)
|
|
256
|
+
if model_id is None and isinstance(model, dict):
|
|
257
|
+
model_id = model.get("id")
|
|
258
|
+
if not model_id:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
display_name = getattr(model, "display_name", None)
|
|
262
|
+
if display_name is None and isinstance(model, dict):
|
|
263
|
+
display_name = model.get("display_name")
|
|
264
|
+
|
|
265
|
+
models.append({"id": model_id, "display_name": display_name})
|
|
266
|
+
|
|
267
|
+
logger.info(
|
|
268
|
+
"OpenAI list_models succeeded: count=%d request_id=%s",
|
|
269
|
+
len(models),
|
|
270
|
+
request_id,
|
|
271
|
+
)
|
|
272
|
+
return models
|
|
273
|
+
|
|
274
|
+
return run_with_retries(
|
|
275
|
+
func=_run_request,
|
|
276
|
+
max_retries=retry_count,
|
|
277
|
+
retry_backoff_s=retry_backoff_s,
|
|
278
|
+
request_id=request_id,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
async def async_list_models(
|
|
282
|
+
self,
|
|
283
|
+
*,
|
|
284
|
+
api_key: str,
|
|
285
|
+
request_id: Optional[str] = None,
|
|
286
|
+
timeout_s: Optional[float] = None,
|
|
287
|
+
max_retries: Optional[int] = None,
|
|
288
|
+
retry_backoff_s: float = 0.5,
|
|
289
|
+
) -> list[dict[str, Optional[str]]]:
|
|
290
|
+
return await run_sync_in_thread(
|
|
291
|
+
lambda: self.list_models(
|
|
292
|
+
api_key=api_key,
|
|
293
|
+
request_id=request_id,
|
|
294
|
+
timeout_s=timeout_s,
|
|
295
|
+
max_retries=max_retries,
|
|
296
|
+
retry_backoff_s=retry_backoff_s,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
167
299
|
|
|
168
300
|
|
|
169
301
|
def _encode_image_path(path: Path) -> str:
|