ccs-llmconnector 1.1.2__py3-none-any.whl → 1.1.4__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.1.2.dist-info → ccs_llmconnector-1.1.4.dist-info}/METADATA +1 -1
- ccs_llmconnector-1.1.4.dist-info/RECORD +16 -0
- {ccs_llmconnector-1.1.2.dist-info → ccs_llmconnector-1.1.4.dist-info}/WHEEL +1 -1
- llmconnector/__init__.py +23 -21
- llmconnector/anthropic_client.py +266 -266
- llmconnector/client.py +566 -301
- llmconnector/client_cli.py +42 -42
- llmconnector/gemini_client.py +411 -96
- llmconnector/grok_client.py +270 -270
- llmconnector/openai_client.py +407 -263
- llmconnector/types.py +66 -48
- llmconnector/utils.py +77 -77
- ccs_llmconnector-1.1.2.dist-info/RECORD +0 -16
- {ccs_llmconnector-1.1.2.dist-info → ccs_llmconnector-1.1.4.dist-info}/entry_points.txt +0 -0
- {ccs_llmconnector-1.1.2.dist-info → ccs_llmconnector-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {ccs_llmconnector-1.1.2.dist-info → ccs_llmconnector-1.1.4.dist-info}/top_level.txt +0 -0
llmconnector/openai_client.py
CHANGED
|
@@ -4,164 +4,256 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import mimetypes
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
import logging
|
|
9
|
-
from typing import Optional, Sequence
|
|
10
|
-
|
|
11
|
-
from openai import OpenAI
|
|
12
|
-
|
|
13
|
-
from .types import ImageInput, MessageSequence, normalize_messages
|
|
14
|
-
from .utils import clamp_retries, run_sync_in_thread, run_with_retries
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class OpenAIResponsesClient:
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional, Sequence
|
|
10
|
+
|
|
11
|
+
from openai import OpenAI
|
|
12
|
+
|
|
13
|
+
from .types import ImageInput, LLMResponse, MessageSequence, TokenUsage, normalize_messages
|
|
14
|
+
from .utils import clamp_retries, run_sync_in_thread, run_with_retries
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OpenAIResponsesClient:
|
|
20
20
|
"""Convenience wrapper around the OpenAI Responses API."""
|
|
21
21
|
|
|
22
|
-
def generate_response(
|
|
23
|
-
self,
|
|
24
|
-
*,
|
|
25
|
-
api_key: str,
|
|
26
|
-
prompt: Optional[str] = None,
|
|
27
|
-
model: str,
|
|
28
|
-
max_tokens: int = 32000,
|
|
29
|
-
reasoning_effort: Optional[str] = None,
|
|
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,
|
|
36
|
-
) -> str:
|
|
37
|
-
"""Generate a response from the specified model.
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
api_key: Secret key used to authenticate with OpenAI.
|
|
41
|
-
prompt: Natural-language instruction or query for the model.
|
|
42
|
-
model: Identifier of the OpenAI model to target (for example, ``"gpt-4o"``).
|
|
43
|
-
max_tokens: Cap for tokens across the entire exchange, defaults to 32000.
|
|
44
|
-
reasoning_effort: Optional reasoning effort hint (``"low"``, ``"medium"``, or ``"high"``).
|
|
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.
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
The text output produced by the model.
|
|
54
|
-
|
|
22
|
+
def generate_response(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
api_key: str,
|
|
26
|
+
prompt: Optional[str] = None,
|
|
27
|
+
model: str,
|
|
28
|
+
max_tokens: int = 32000,
|
|
29
|
+
reasoning_effort: Optional[str] = None,
|
|
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,
|
|
36
|
+
) -> str:
|
|
37
|
+
"""Generate a response from the specified model.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
api_key: Secret key used to authenticate with OpenAI.
|
|
41
|
+
prompt: Natural-language instruction or query for the model.
|
|
42
|
+
model: Identifier of the OpenAI model to target (for example, ``"gpt-4o"``).
|
|
43
|
+
max_tokens: Cap for tokens across the entire exchange, defaults to 32000.
|
|
44
|
+
reasoning_effort: Optional reasoning effort hint (``"low"``, ``"medium"``, or ``"high"``).
|
|
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.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The text output produced by the model.
|
|
54
|
+
|
|
55
55
|
Raises:
|
|
56
56
|
ValueError: If required arguments are missing or the request payload is empty.
|
|
57
57
|
OpenAIError: If the underlying OpenAI request fails.
|
|
58
|
-
"""
|
|
59
|
-
if not api_key:
|
|
60
|
-
raise ValueError("api_key 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.")
|
|
63
|
-
if not model:
|
|
64
|
-
raise ValueError("model must be provided.")
|
|
65
|
-
|
|
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})
|
|
73
|
-
|
|
74
|
-
if images:
|
|
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):
|
|
90
|
-
raise ValueError("No content provided for response generation.")
|
|
91
|
-
|
|
92
|
-
request_payload = {
|
|
93
|
-
"model": model,
|
|
94
|
-
"input": content_messages,
|
|
95
|
-
"max_output_tokens": max_tokens,
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if reasoning_effort:
|
|
99
|
-
request_payload["reasoning"] = {"effort": reasoning_effort}
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
) ->
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
164
|
-
|
|
58
|
+
"""
|
|
59
|
+
if not api_key:
|
|
60
|
+
raise ValueError("api_key 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.")
|
|
63
|
+
if not model:
|
|
64
|
+
raise ValueError("model must be provided.")
|
|
65
|
+
|
|
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})
|
|
73
|
+
|
|
74
|
+
if images:
|
|
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):
|
|
90
|
+
raise ValueError("No content provided for response generation.")
|
|
91
|
+
|
|
92
|
+
request_payload = {
|
|
93
|
+
"model": model,
|
|
94
|
+
"input": content_messages,
|
|
95
|
+
"max_output_tokens": max_tokens,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if reasoning_effort:
|
|
99
|
+
request_payload["reasoning"] = {"effort": reasoning_effort}
|
|
100
|
+
|
|
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
|
+
def generate_response_with_usage(
|
|
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
|
+
) -> LLMResponse:
|
|
150
|
+
if not api_key:
|
|
151
|
+
raise ValueError("api_key must be provided.")
|
|
152
|
+
if not prompt and not messages and not images:
|
|
153
|
+
raise ValueError("At least one of prompt, messages, or images must be provided.")
|
|
154
|
+
if not model:
|
|
155
|
+
raise ValueError("model must be provided.")
|
|
156
|
+
|
|
157
|
+
normalized_messages = normalize_messages(prompt=prompt, messages=messages)
|
|
158
|
+
content_messages: list[dict] = []
|
|
159
|
+
for message in normalized_messages:
|
|
160
|
+
content_blocks = []
|
|
161
|
+
if message["content"]:
|
|
162
|
+
content_blocks.append({"type": "input_text", "text": message["content"]})
|
|
163
|
+
content_messages.append({"role": message["role"], "content": content_blocks})
|
|
164
|
+
|
|
165
|
+
if images:
|
|
166
|
+
image_blocks = [self._to_image_block(image) for image in images]
|
|
167
|
+
target_index = next(
|
|
168
|
+
(
|
|
169
|
+
index
|
|
170
|
+
for index in range(len(content_messages) - 1, -1, -1)
|
|
171
|
+
if content_messages[index]["role"] == "user"
|
|
172
|
+
),
|
|
173
|
+
None,
|
|
174
|
+
)
|
|
175
|
+
if target_index is None:
|
|
176
|
+
content_messages.append({"role": "user", "content": image_blocks})
|
|
177
|
+
else:
|
|
178
|
+
content_messages[target_index]["content"].extend(image_blocks)
|
|
179
|
+
|
|
180
|
+
if not any(message["content"] for message in content_messages):
|
|
181
|
+
raise ValueError("No content provided for response generation.")
|
|
182
|
+
|
|
183
|
+
request_payload = {
|
|
184
|
+
"model": model,
|
|
185
|
+
"input": content_messages,
|
|
186
|
+
"max_output_tokens": max_tokens,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if reasoning_effort:
|
|
190
|
+
request_payload["reasoning"] = {"effort": reasoning_effort}
|
|
191
|
+
|
|
192
|
+
retry_count = clamp_retries(max_retries)
|
|
193
|
+
|
|
194
|
+
def _run_request() -> LLMResponse:
|
|
195
|
+
client_kwargs = {"api_key": api_key}
|
|
196
|
+
if timeout_s is not None:
|
|
197
|
+
client_kwargs["timeout"] = timeout_s
|
|
198
|
+
client = OpenAI(**client_kwargs)
|
|
199
|
+
try:
|
|
200
|
+
response = client.responses.create(**request_payload)
|
|
201
|
+
except Exception as exc: # Log and re-raise to preserve default behavior
|
|
202
|
+
logger.exception(
|
|
203
|
+
"OpenAI Responses API request failed: %s request_id=%s",
|
|
204
|
+
exc,
|
|
205
|
+
request_id,
|
|
206
|
+
)
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
output_text = response.output_text
|
|
210
|
+
usage = _extract_openai_usage(response)
|
|
211
|
+
logger.info(
|
|
212
|
+
"OpenAI generate_response_with_usage succeeded: model=%s images=%d text_len=%d request_id=%s",
|
|
213
|
+
model,
|
|
214
|
+
len(images or []),
|
|
215
|
+
len(output_text or ""),
|
|
216
|
+
request_id,
|
|
217
|
+
)
|
|
218
|
+
return LLMResponse(text=output_text, usage=usage, provider="openai", model=model)
|
|
219
|
+
|
|
220
|
+
return run_with_retries(
|
|
221
|
+
func=_run_request,
|
|
222
|
+
max_retries=retry_count,
|
|
223
|
+
retry_backoff_s=retry_backoff_s,
|
|
224
|
+
request_id=request_id,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def async_generate_response(
|
|
228
|
+
self,
|
|
229
|
+
*,
|
|
230
|
+
api_key: str,
|
|
231
|
+
prompt: Optional[str] = None,
|
|
232
|
+
model: str,
|
|
233
|
+
max_tokens: int = 32000,
|
|
234
|
+
reasoning_effort: Optional[str] = None,
|
|
235
|
+
images: Optional[Sequence[ImageInput]] = None,
|
|
236
|
+
messages: Optional[MessageSequence] = None,
|
|
237
|
+
request_id: Optional[str] = None,
|
|
238
|
+
timeout_s: Optional[float] = None,
|
|
239
|
+
max_retries: Optional[int] = None,
|
|
240
|
+
retry_backoff_s: float = 0.5,
|
|
241
|
+
) -> str:
|
|
242
|
+
return await run_sync_in_thread(
|
|
243
|
+
lambda: self.generate_response(
|
|
244
|
+
api_key=api_key,
|
|
245
|
+
prompt=prompt,
|
|
246
|
+
model=model,
|
|
247
|
+
max_tokens=max_tokens,
|
|
248
|
+
reasoning_effort=reasoning_effort,
|
|
249
|
+
images=images,
|
|
250
|
+
messages=messages,
|
|
251
|
+
request_id=request_id,
|
|
252
|
+
timeout_s=timeout_s,
|
|
253
|
+
max_retries=max_retries,
|
|
254
|
+
retry_backoff_s=retry_backoff_s,
|
|
255
|
+
)
|
|
256
|
+
)
|
|
165
257
|
|
|
166
258
|
@staticmethod
|
|
167
259
|
def _to_image_block(image: ImageInput) -> dict:
|
|
@@ -183,11 +275,11 @@ class OpenAIResponsesClient:
|
|
|
183
275
|
"image_url": _encode_image_path(Path(image)),
|
|
184
276
|
}
|
|
185
277
|
|
|
186
|
-
def generate_image(
|
|
187
|
-
self,
|
|
188
|
-
*,
|
|
189
|
-
api_key: str,
|
|
190
|
-
prompt: str,
|
|
278
|
+
def generate_image(
|
|
279
|
+
self,
|
|
280
|
+
*,
|
|
281
|
+
api_key: str,
|
|
282
|
+
prompt: str,
|
|
191
283
|
model: str,
|
|
192
284
|
image_size: Optional[str] = None,
|
|
193
285
|
aspect_ratio: Optional[str] = None,
|
|
@@ -195,112 +287,164 @@ class OpenAIResponsesClient:
|
|
|
195
287
|
) -> bytes:
|
|
196
288
|
"""Generate an image using the OpenAI API.
|
|
197
289
|
|
|
198
|
-
Raises:
|
|
199
|
-
NotImplementedError: This method is not yet implemented for OpenAI.
|
|
200
|
-
"""
|
|
201
|
-
raise NotImplementedError("Image generation is not implemented for OpenAI.")
|
|
202
|
-
|
|
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
|
|
225
|
-
self,
|
|
226
|
-
*,
|
|
227
|
-
api_key: str,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
290
|
+
Raises:
|
|
291
|
+
NotImplementedError: This method is not yet implemented for OpenAI.
|
|
292
|
+
"""
|
|
293
|
+
raise NotImplementedError("Image generation is not implemented for OpenAI.")
|
|
294
|
+
|
|
295
|
+
async def async_generate_image(
|
|
296
|
+
self,
|
|
297
|
+
*,
|
|
298
|
+
api_key: str,
|
|
299
|
+
prompt: str,
|
|
300
|
+
model: str,
|
|
301
|
+
image_size: Optional[str] = None,
|
|
302
|
+
aspect_ratio: Optional[str] = None,
|
|
303
|
+
image: Optional[ImageInput] = None,
|
|
304
|
+
) -> bytes:
|
|
305
|
+
return await run_sync_in_thread(
|
|
306
|
+
lambda: self.generate_image(
|
|
307
|
+
api_key=api_key,
|
|
308
|
+
prompt=prompt,
|
|
309
|
+
model=model,
|
|
310
|
+
image_size=image_size,
|
|
311
|
+
aspect_ratio=aspect_ratio,
|
|
312
|
+
image=image,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
async def async_generate_response_with_usage(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
api_key: str,
|
|
320
|
+
prompt: Optional[str] = None,
|
|
321
|
+
model: str,
|
|
322
|
+
max_tokens: int = 32000,
|
|
323
|
+
reasoning_effort: Optional[str] = None,
|
|
324
|
+
images: Optional[Sequence[ImageInput]] = None,
|
|
325
|
+
messages: Optional[MessageSequence] = None,
|
|
326
|
+
request_id: Optional[str] = None,
|
|
327
|
+
timeout_s: Optional[float] = None,
|
|
328
|
+
max_retries: Optional[int] = None,
|
|
329
|
+
retry_backoff_s: float = 0.5,
|
|
330
|
+
) -> LLMResponse:
|
|
331
|
+
return await run_sync_in_thread(
|
|
332
|
+
lambda: self.generate_response_with_usage(
|
|
333
|
+
api_key=api_key,
|
|
334
|
+
prompt=prompt,
|
|
335
|
+
model=model,
|
|
336
|
+
max_tokens=max_tokens,
|
|
337
|
+
reasoning_effort=reasoning_effort,
|
|
338
|
+
images=images,
|
|
339
|
+
messages=messages,
|
|
340
|
+
request_id=request_id,
|
|
341
|
+
timeout_s=timeout_s,
|
|
342
|
+
max_retries=max_retries,
|
|
343
|
+
retry_backoff_s=retry_backoff_s,
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def list_models(
|
|
348
|
+
self,
|
|
349
|
+
*,
|
|
350
|
+
api_key: str,
|
|
351
|
+
request_id: Optional[str] = None,
|
|
352
|
+
timeout_s: Optional[float] = None,
|
|
353
|
+
max_retries: Optional[int] = None,
|
|
354
|
+
retry_backoff_s: float = 0.5,
|
|
355
|
+
) -> list[dict[str, Optional[str]]]:
|
|
356
|
+
"""Return the models available to the authenticated OpenAI account."""
|
|
357
|
+
if not api_key:
|
|
358
|
+
raise ValueError("api_key must be provided.")
|
|
359
|
+
|
|
360
|
+
retry_count = clamp_retries(max_retries)
|
|
361
|
+
|
|
362
|
+
def _run_request() -> list[dict[str, Optional[str]]]:
|
|
363
|
+
client_kwargs = {"api_key": api_key}
|
|
364
|
+
if timeout_s is not None:
|
|
365
|
+
client_kwargs["timeout"] = timeout_s
|
|
366
|
+
client = OpenAI(**client_kwargs)
|
|
367
|
+
try:
|
|
368
|
+
response = client.models.list()
|
|
369
|
+
except Exception as exc:
|
|
370
|
+
logger.exception(
|
|
371
|
+
"OpenAI list models failed: %s request_id=%s", exc, request_id
|
|
372
|
+
)
|
|
373
|
+
raise
|
|
374
|
+
data = getattr(response, "data", []) or []
|
|
375
|
+
|
|
376
|
+
models: list[dict[str, Optional[str]]] = []
|
|
377
|
+
for model in data:
|
|
378
|
+
model_id = getattr(model, "id", None)
|
|
379
|
+
if model_id is None and isinstance(model, dict):
|
|
380
|
+
model_id = model.get("id")
|
|
381
|
+
if not model_id:
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
display_name = getattr(model, "display_name", None)
|
|
385
|
+
if display_name is None and isinstance(model, dict):
|
|
386
|
+
display_name = model.get("display_name")
|
|
387
|
+
|
|
388
|
+
models.append({"id": model_id, "display_name": display_name})
|
|
389
|
+
|
|
390
|
+
logger.info(
|
|
391
|
+
"OpenAI list_models succeeded: count=%d request_id=%s",
|
|
392
|
+
len(models),
|
|
393
|
+
request_id,
|
|
394
|
+
)
|
|
395
|
+
return models
|
|
396
|
+
|
|
397
|
+
return run_with_retries(
|
|
398
|
+
func=_run_request,
|
|
399
|
+
max_retries=retry_count,
|
|
400
|
+
retry_backoff_s=retry_backoff_s,
|
|
401
|
+
request_id=request_id,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
async def async_list_models(
|
|
405
|
+
self,
|
|
406
|
+
*,
|
|
407
|
+
api_key: str,
|
|
408
|
+
request_id: Optional[str] = None,
|
|
409
|
+
timeout_s: Optional[float] = None,
|
|
410
|
+
max_retries: Optional[int] = None,
|
|
411
|
+
retry_backoff_s: float = 0.5,
|
|
412
|
+
) -> list[dict[str, Optional[str]]]:
|
|
413
|
+
return await run_sync_in_thread(
|
|
414
|
+
lambda: self.list_models(
|
|
415
|
+
api_key=api_key,
|
|
416
|
+
request_id=request_id,
|
|
417
|
+
timeout_s=timeout_s,
|
|
418
|
+
max_retries=max_retries,
|
|
419
|
+
retry_backoff_s=retry_backoff_s,
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _encode_image_path(path: Path) -> str:
|
|
302
425
|
"""Generate a data URL for the provided image path."""
|
|
303
426
|
data = path.read_bytes()
|
|
304
427
|
encoded = base64.b64encode(data).decode("utf-8")
|
|
305
428
|
mime_type = mimetypes.guess_type(path.name)[0] or "image/png"
|
|
306
|
-
return f"data:{mime_type};base64,{encoded}"
|
|
429
|
+
return f"data:{mime_type};base64,{encoded}"
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _extract_openai_usage(response: object) -> TokenUsage | None:
|
|
433
|
+
usage_obj = getattr(response, "usage", None)
|
|
434
|
+
if usage_obj is None:
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
input_tokens = getattr(usage_obj, "input_tokens", None)
|
|
438
|
+
output_tokens = getattr(usage_obj, "output_tokens", None)
|
|
439
|
+
total_tokens = getattr(usage_obj, "total_tokens", None)
|
|
440
|
+
|
|
441
|
+
if isinstance(usage_obj, dict):
|
|
442
|
+
input_tokens = usage_obj.get("input_tokens")
|
|
443
|
+
output_tokens = usage_obj.get("output_tokens")
|
|
444
|
+
total_tokens = usage_obj.get("total_tokens")
|
|
445
|
+
|
|
446
|
+
return TokenUsage(
|
|
447
|
+
input_tokens=int(input_tokens) if isinstance(input_tokens, int) else None,
|
|
448
|
+
output_tokens=int(output_tokens) if isinstance(output_tokens, int) else None,
|
|
449
|
+
total_tokens=int(total_tokens) if isinstance(total_tokens, int) else None,
|
|
450
|
+
)
|