pixeltable 0.3.2__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of pixeltable might be problematic. Click here for more details.
- pixeltable/__init__.py +64 -11
- pixeltable/__version__.py +2 -2
- pixeltable/catalog/__init__.py +1 -1
- pixeltable/catalog/catalog.py +50 -27
- pixeltable/catalog/column.py +27 -11
- pixeltable/catalog/dir.py +6 -4
- pixeltable/catalog/globals.py +8 -1
- pixeltable/catalog/insertable_table.py +22 -12
- pixeltable/catalog/named_function.py +10 -6
- pixeltable/catalog/path.py +3 -2
- pixeltable/catalog/path_dict.py +8 -6
- pixeltable/catalog/schema_object.py +2 -1
- pixeltable/catalog/table.py +121 -101
- pixeltable/catalog/table_version.py +291 -142
- pixeltable/catalog/table_version_path.py +8 -5
- pixeltable/catalog/view.py +67 -26
- pixeltable/dataframe.py +106 -81
- pixeltable/env.py +28 -24
- pixeltable/exec/__init__.py +2 -2
- pixeltable/exec/aggregation_node.py +10 -4
- pixeltable/exec/cache_prefetch_node.py +5 -3
- pixeltable/exec/component_iteration_node.py +9 -9
- pixeltable/exec/data_row_batch.py +21 -10
- pixeltable/exec/exec_context.py +10 -3
- pixeltable/exec/exec_node.py +23 -12
- pixeltable/exec/expr_eval/evaluators.py +13 -7
- pixeltable/exec/expr_eval/expr_eval_node.py +24 -15
- pixeltable/exec/expr_eval/globals.py +30 -7
- pixeltable/exec/expr_eval/row_buffer.py +5 -6
- pixeltable/exec/expr_eval/schedulers.py +151 -31
- pixeltable/exec/in_memory_data_node.py +8 -7
- pixeltable/exec/row_update_node.py +15 -5
- pixeltable/exec/sql_node.py +56 -27
- pixeltable/exprs/__init__.py +2 -2
- pixeltable/exprs/arithmetic_expr.py +57 -26
- pixeltable/exprs/array_slice.py +1 -1
- pixeltable/exprs/column_property_ref.py +2 -1
- pixeltable/exprs/column_ref.py +20 -15
- pixeltable/exprs/comparison.py +6 -2
- pixeltable/exprs/compound_predicate.py +1 -3
- pixeltable/exprs/data_row.py +2 -2
- pixeltable/exprs/expr.py +108 -72
- pixeltable/exprs/expr_dict.py +2 -1
- pixeltable/exprs/expr_set.py +3 -1
- pixeltable/exprs/function_call.py +39 -41
- pixeltable/exprs/globals.py +1 -0
- pixeltable/exprs/in_predicate.py +2 -2
- pixeltable/exprs/inline_expr.py +20 -17
- pixeltable/exprs/json_mapper.py +4 -2
- pixeltable/exprs/json_path.py +12 -18
- pixeltable/exprs/literal.py +5 -9
- pixeltable/exprs/method_ref.py +1 -0
- pixeltable/exprs/object_ref.py +1 -1
- pixeltable/exprs/row_builder.py +32 -17
- pixeltable/exprs/rowid_ref.py +14 -5
- pixeltable/exprs/similarity_expr.py +11 -6
- pixeltable/exprs/sql_element_cache.py +1 -1
- pixeltable/exprs/type_cast.py +24 -9
- pixeltable/ext/__init__.py +1 -0
- pixeltable/ext/functions/__init__.py +1 -0
- pixeltable/ext/functions/whisperx.py +2 -2
- pixeltable/ext/functions/yolox.py +11 -11
- pixeltable/func/aggregate_function.py +17 -13
- pixeltable/func/callable_function.py +6 -6
- pixeltable/func/expr_template_function.py +15 -14
- pixeltable/func/function.py +16 -16
- pixeltable/func/function_registry.py +11 -8
- pixeltable/func/globals.py +4 -2
- pixeltable/func/query_template_function.py +12 -13
- pixeltable/func/signature.py +18 -9
- pixeltable/func/tools.py +10 -17
- pixeltable/func/udf.py +106 -11
- pixeltable/functions/__init__.py +21 -2
- pixeltable/functions/anthropic.py +16 -12
- pixeltable/functions/fireworks.py +63 -5
- pixeltable/functions/gemini.py +13 -3
- pixeltable/functions/globals.py +18 -6
- pixeltable/functions/huggingface.py +20 -38
- pixeltable/functions/image.py +7 -3
- pixeltable/functions/json.py +1 -0
- pixeltable/functions/llama_cpp.py +1 -4
- pixeltable/functions/mistralai.py +31 -20
- pixeltable/functions/ollama.py +4 -18
- pixeltable/functions/openai.py +231 -113
- pixeltable/functions/replicate.py +11 -10
- pixeltable/functions/string.py +70 -7
- pixeltable/functions/timestamp.py +21 -8
- pixeltable/functions/together.py +66 -52
- pixeltable/functions/video.py +1 -0
- pixeltable/functions/vision.py +14 -11
- pixeltable/functions/whisper.py +2 -1
- pixeltable/globals.py +60 -26
- pixeltable/index/__init__.py +1 -1
- pixeltable/index/btree.py +5 -3
- pixeltable/index/embedding_index.py +15 -14
- pixeltable/io/__init__.py +1 -1
- pixeltable/io/external_store.py +30 -25
- pixeltable/io/fiftyone.py +6 -14
- pixeltable/io/globals.py +33 -27
- pixeltable/io/hf_datasets.py +2 -1
- pixeltable/io/label_studio.py +77 -68
- pixeltable/io/pandas.py +36 -23
- pixeltable/io/parquet.py +9 -12
- pixeltable/iterators/__init__.py +1 -0
- pixeltable/iterators/audio.py +205 -0
- pixeltable/iterators/document.py +19 -8
- pixeltable/iterators/image.py +6 -24
- pixeltable/iterators/string.py +3 -6
- pixeltable/iterators/video.py +1 -7
- pixeltable/metadata/__init__.py +7 -1
- pixeltable/metadata/converters/convert_10.py +2 -2
- pixeltable/metadata/converters/convert_15.py +1 -5
- pixeltable/metadata/converters/convert_16.py +2 -4
- pixeltable/metadata/converters/convert_17.py +2 -4
- pixeltable/metadata/converters/convert_18.py +2 -4
- pixeltable/metadata/converters/convert_19.py +2 -5
- pixeltable/metadata/converters/convert_20.py +1 -4
- pixeltable/metadata/converters/convert_21.py +4 -6
- pixeltable/metadata/converters/convert_22.py +1 -0
- pixeltable/metadata/converters/convert_23.py +5 -5
- pixeltable/metadata/converters/convert_24.py +12 -13
- pixeltable/metadata/converters/convert_26.py +23 -0
- pixeltable/metadata/converters/util.py +3 -4
- pixeltable/metadata/notes.py +1 -0
- pixeltable/metadata/schema.py +13 -2
- pixeltable/plan.py +173 -98
- pixeltable/share/__init__.py +0 -0
- pixeltable/share/packager.py +218 -0
- pixeltable/store.py +42 -26
- pixeltable/type_system.py +102 -75
- pixeltable/utils/arrow.py +7 -8
- pixeltable/utils/coco.py +16 -17
- pixeltable/utils/code.py +1 -1
- pixeltable/utils/console_output.py +6 -3
- pixeltable/utils/description_helper.py +7 -7
- pixeltable/utils/documents.py +3 -1
- pixeltable/utils/filecache.py +12 -7
- pixeltable/utils/http_server.py +9 -8
- pixeltable/utils/iceberg.py +14 -0
- pixeltable/utils/media_store.py +3 -2
- pixeltable/utils/pytorch.py +11 -14
- pixeltable/utils/s3.py +1 -0
- pixeltable/utils/sql.py +1 -0
- pixeltable/utils/transactional_directory.py +2 -2
- {pixeltable-0.3.2.dist-info → pixeltable-0.3.4.dist-info}/METADATA +9 -9
- pixeltable-0.3.4.dist-info/RECORD +166 -0
- pixeltable-0.3.2.dist-info/RECORD +0 -161
- {pixeltable-0.3.2.dist-info → pixeltable-0.3.4.dist-info}/LICENSE +0 -0
- {pixeltable-0.3.2.dist-info → pixeltable-0.3.4.dist-info}/WHEEL +0 -0
- {pixeltable-0.3.2.dist-info → pixeltable-0.3.4.dist-info}/entry_points.txt +0 -0
pixeltable/functions/openai.py
CHANGED
|
@@ -10,15 +10,15 @@ import datetime
|
|
|
10
10
|
import io
|
|
11
11
|
import json
|
|
12
12
|
import logging
|
|
13
|
+
import math
|
|
13
14
|
import pathlib
|
|
14
15
|
import re
|
|
15
16
|
import uuid
|
|
16
|
-
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, Union, cast
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Type, TypeVar, Union, cast
|
|
17
18
|
|
|
18
|
-
import PIL.Image
|
|
19
19
|
import httpx
|
|
20
20
|
import numpy as np
|
|
21
|
-
import
|
|
21
|
+
import PIL
|
|
22
22
|
|
|
23
23
|
import pixeltable as pxt
|
|
24
24
|
from pixeltable import env, exprs
|
|
@@ -32,36 +32,18 @@ _logger = logging.getLogger('pixeltable')
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@env.register_client('openai')
|
|
35
|
-
def _(api_key: str) ->
|
|
35
|
+
def _(api_key: str) -> 'openai.AsyncOpenAI':
|
|
36
36
|
import openai
|
|
37
|
-
return (
|
|
38
|
-
openai.OpenAI(api_key=api_key),
|
|
39
|
-
openai.AsyncOpenAI(
|
|
40
|
-
api_key=api_key,
|
|
41
|
-
# recommended to increase limits for async client to avoid connection errors
|
|
42
|
-
http_client=httpx.AsyncClient(limits=httpx.Limits(max_keepalive_connections=100, max_connections=500)),
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _openai_client() -> 'openai.OpenAI':
|
|
48
|
-
return env.Env.get().get_client('openai')[0]
|
|
49
|
-
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
38
|
+
return openai.AsyncOpenAI(
|
|
39
|
+
api_key=api_key,
|
|
40
|
+
# recommended to increase limits for async client to avoid connection errors
|
|
41
|
+
http_client=httpx.AsyncClient(limits=httpx.Limits(max_keepalive_connections=100, max_connections=500)),
|
|
42
|
+
)
|
|
53
43
|
|
|
54
44
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# by OpenAI. Should we investigate making this more customizable in the future?
|
|
58
|
-
def _retry(fn: Callable) -> Callable:
|
|
59
|
-
import openai
|
|
60
|
-
return tenacity.retry(
|
|
61
|
-
retry=tenacity.retry_if_exception_type(openai.RateLimitError),
|
|
62
|
-
wait=tenacity.wait_random_exponential(multiplier=1, max=60),
|
|
63
|
-
stop=tenacity.stop_after_attempt(20),
|
|
64
|
-
)(fn)
|
|
45
|
+
def _openai_client() -> 'openai.AsyncOpenAI':
|
|
46
|
+
return env.Env.get().get_client('openai')
|
|
65
47
|
|
|
66
48
|
|
|
67
49
|
# models that share rate limits; see https://platform.openai.com/settings/organization/limits for details
|
|
@@ -72,7 +54,7 @@ _shared_rate_limits = {
|
|
|
72
54
|
'gpt-4-turbo-2024-04-09',
|
|
73
55
|
'gpt-4-turbo-preview',
|
|
74
56
|
'gpt-4-0125-preview',
|
|
75
|
-
'gpt-4-1106-preview'
|
|
57
|
+
'gpt-4-1106-preview',
|
|
76
58
|
],
|
|
77
59
|
'gpt-4o': [
|
|
78
60
|
'gpt-4o',
|
|
@@ -82,24 +64,24 @@ _shared_rate_limits = {
|
|
|
82
64
|
'gpt-4o-2024-11-20',
|
|
83
65
|
'gpt-4o-audio-preview',
|
|
84
66
|
'gpt-4o-audio-preview-2024-10-01',
|
|
85
|
-
'gpt-4o-audio-preview-2024-12-17'
|
|
67
|
+
'gpt-4o-audio-preview-2024-12-17',
|
|
86
68
|
],
|
|
87
69
|
'gpt-4o-mini': [
|
|
88
70
|
'gpt-4o-mini',
|
|
89
71
|
'gpt-4o-mini-latest',
|
|
90
72
|
'gpt-4o-mini-2024-07-18',
|
|
91
73
|
'gpt-4o-mini-audio-preview',
|
|
92
|
-
'gpt-4o-mini-audio-preview-2024-12-17'
|
|
74
|
+
'gpt-4o-mini-audio-preview-2024-12-17',
|
|
93
75
|
],
|
|
94
76
|
'gpt-4o-mini-realtime-preview': [
|
|
95
77
|
'gpt-4o-mini-realtime-preview',
|
|
96
78
|
'gpt-4o-mini-realtime-preview-latest',
|
|
97
|
-
'gpt-4o-mini-realtime-preview-2024-12-17'
|
|
98
|
-
]
|
|
79
|
+
'gpt-4o-mini-realtime-preview-2024-12-17',
|
|
80
|
+
],
|
|
99
81
|
}
|
|
100
82
|
|
|
101
83
|
|
|
102
|
-
def
|
|
84
|
+
def _rate_limits_pool(model: str) -> str:
|
|
103
85
|
for model_family, models in _shared_rate_limits.items():
|
|
104
86
|
if model in models:
|
|
105
87
|
return f'rate-limits:openai:{model_family}'
|
|
@@ -112,13 +94,13 @@ class OpenAIRateLimitsInfo(env.RateLimitsInfo):
|
|
|
112
94
|
def __init__(self, get_request_resources: Callable[..., dict[str, int]]):
|
|
113
95
|
super().__init__(get_request_resources)
|
|
114
96
|
import openai
|
|
97
|
+
|
|
115
98
|
self.retryable_errors = (
|
|
116
99
|
# ConnectionError: we occasionally see this error when the AsyncConnectionPool is trying to close
|
|
117
100
|
# expired connections
|
|
118
101
|
# (AsyncConnectionPool._close_expired_connections() fails with ConnectionError when executing
|
|
119
|
-
# 'await connection.aclose()', which is
|
|
102
|
+
# 'await connection.aclose()', which is very likely a bug in AsyncConnectionPool)
|
|
120
103
|
openai.APIConnectionError,
|
|
121
|
-
|
|
122
104
|
# the following errors are retryable according to OpenAI's API documentation
|
|
123
105
|
openai.RateLimitError,
|
|
124
106
|
openai.APITimeoutError,
|
|
@@ -143,7 +125,7 @@ _header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d
|
|
|
143
125
|
def _parse_header_duration(duration_str):
|
|
144
126
|
match = _header_duration_pattern.match(duration_str)
|
|
145
127
|
if not match:
|
|
146
|
-
raise ValueError(
|
|
128
|
+
raise ValueError('Invalid duration format')
|
|
147
129
|
|
|
148
130
|
days = int(match.group(1) or 0)
|
|
149
131
|
hours = int(match.group(2) or 0)
|
|
@@ -151,17 +133,11 @@ def _parse_header_duration(duration_str):
|
|
|
151
133
|
minutes = int(match.group(4) or 0)
|
|
152
134
|
seconds = float(match.group(5) or 0)
|
|
153
135
|
|
|
154
|
-
return datetime.timedelta(
|
|
155
|
-
days=days,
|
|
156
|
-
hours=hours,
|
|
157
|
-
minutes=minutes,
|
|
158
|
-
seconds=seconds,
|
|
159
|
-
milliseconds=milliseconds
|
|
160
|
-
)
|
|
136
|
+
return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
|
|
161
137
|
|
|
162
138
|
|
|
163
139
|
def _get_header_info(
|
|
164
|
-
|
|
140
|
+
headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
|
|
165
141
|
) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
|
|
166
142
|
assert requests or tokens
|
|
167
143
|
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
@@ -194,8 +170,14 @@ def _get_header_info(
|
|
|
194
170
|
|
|
195
171
|
|
|
196
172
|
@pxt.udf
|
|
197
|
-
def speech(
|
|
198
|
-
|
|
173
|
+
async def speech(
|
|
174
|
+
input: str,
|
|
175
|
+
*,
|
|
176
|
+
model: str,
|
|
177
|
+
voice: str,
|
|
178
|
+
response_format: Optional[str] = None,
|
|
179
|
+
speed: Optional[float] = None,
|
|
180
|
+
timeout: Optional[float] = None,
|
|
199
181
|
) -> pxt.Audio:
|
|
200
182
|
"""
|
|
201
183
|
Generates audio from the input text.
|
|
@@ -203,6 +185,10 @@ def speech(
|
|
|
203
185
|
Equivalent to the OpenAI `audio/speech` API endpoint.
|
|
204
186
|
For additional details, see: [https://platform.openai.com/docs/guides/text-to-speech](https://platform.openai.com/docs/guides/text-to-speech)
|
|
205
187
|
|
|
188
|
+
Request throttling:
|
|
189
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
190
|
+
limit is configured, uses a default of 600 RPM.
|
|
191
|
+
|
|
206
192
|
__Requirements:__
|
|
207
193
|
|
|
208
194
|
- `pip install openai`
|
|
@@ -222,10 +208,15 @@ def speech(
|
|
|
222
208
|
Add a computed column that applies the model `tts-1` to an existing Pixeltable column `tbl.text`
|
|
223
209
|
of the table `tbl`:
|
|
224
210
|
|
|
225
|
-
>>> tbl
|
|
211
|
+
>>> tbl.add_computed_column(audio=speech(tbl.text, model='tts-1', voice='nova'))
|
|
226
212
|
"""
|
|
227
|
-
content =
|
|
228
|
-
input=input,
|
|
213
|
+
content = await _openai_client().audio.speech.create(
|
|
214
|
+
input=input,
|
|
215
|
+
model=model,
|
|
216
|
+
voice=voice, # type: ignore
|
|
217
|
+
response_format=_opt(response_format), # type: ignore
|
|
218
|
+
speed=_opt(speed),
|
|
219
|
+
timeout=_opt(timeout),
|
|
229
220
|
)
|
|
230
221
|
ext = response_format or 'mp3'
|
|
231
222
|
output_filename = str(env.Env.get().tmp_dir / f'{uuid.uuid4()}.{ext}')
|
|
@@ -234,13 +225,14 @@ def speech(
|
|
|
234
225
|
|
|
235
226
|
|
|
236
227
|
@pxt.udf
|
|
237
|
-
def transcriptions(
|
|
228
|
+
async def transcriptions(
|
|
238
229
|
audio: pxt.Audio,
|
|
239
230
|
*,
|
|
240
231
|
model: str,
|
|
241
232
|
language: Optional[str] = None,
|
|
242
233
|
prompt: Optional[str] = None,
|
|
243
234
|
temperature: Optional[float] = None,
|
|
235
|
+
timeout: Optional[float] = None,
|
|
244
236
|
) -> dict:
|
|
245
237
|
"""
|
|
246
238
|
Transcribes audio into the input language.
|
|
@@ -248,6 +240,10 @@ def transcriptions(
|
|
|
248
240
|
Equivalent to the OpenAI `audio/transcriptions` API endpoint.
|
|
249
241
|
For additional details, see: [https://platform.openai.com/docs/guides/speech-to-text](https://platform.openai.com/docs/guides/speech-to-text)
|
|
250
242
|
|
|
243
|
+
Request throttling:
|
|
244
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
245
|
+
limit is configured, uses a default of 600 RPM.
|
|
246
|
+
|
|
251
247
|
__Requirements:__
|
|
252
248
|
|
|
253
249
|
- `pip install openai`
|
|
@@ -265,22 +261,28 @@ def transcriptions(
|
|
|
265
261
|
Add a computed column that applies the model `whisper-1` to an existing Pixeltable column `tbl.audio`
|
|
266
262
|
of the table `tbl`:
|
|
267
263
|
|
|
268
|
-
>>> tbl
|
|
264
|
+
>>> tbl.add_computed_column(transcription=transcriptions(tbl.audio, model='whisper-1', language='en'))
|
|
269
265
|
"""
|
|
270
266
|
file = pathlib.Path(audio)
|
|
271
|
-
transcription =
|
|
272
|
-
file=file,
|
|
267
|
+
transcription = await _openai_client().audio.transcriptions.create(
|
|
268
|
+
file=file,
|
|
269
|
+
model=model,
|
|
270
|
+
language=_opt(language),
|
|
271
|
+
prompt=_opt(prompt),
|
|
272
|
+
temperature=_opt(temperature),
|
|
273
|
+
timeout=_opt(timeout),
|
|
273
274
|
)
|
|
274
275
|
return transcription.dict()
|
|
275
276
|
|
|
276
277
|
|
|
277
278
|
@pxt.udf
|
|
278
|
-
def translations(
|
|
279
|
+
async def translations(
|
|
279
280
|
audio: pxt.Audio,
|
|
280
281
|
*,
|
|
281
282
|
model: str,
|
|
282
283
|
prompt: Optional[str] = None,
|
|
283
|
-
temperature: Optional[float] = None
|
|
284
|
+
temperature: Optional[float] = None,
|
|
285
|
+
timeout: Optional[float] = None,
|
|
284
286
|
) -> dict:
|
|
285
287
|
"""
|
|
286
288
|
Translates audio into English.
|
|
@@ -288,6 +290,10 @@ def translations(
|
|
|
288
290
|
Equivalent to the OpenAI `audio/translations` API endpoint.
|
|
289
291
|
For additional details, see: [https://platform.openai.com/docs/guides/speech-to-text](https://platform.openai.com/docs/guides/speech-to-text)
|
|
290
292
|
|
|
293
|
+
Request throttling:
|
|
294
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
295
|
+
limit is configured, uses a default of 600 RPM.
|
|
296
|
+
|
|
291
297
|
__Requirements:__
|
|
292
298
|
|
|
293
299
|
- `pip install openai`
|
|
@@ -305,11 +311,11 @@ def translations(
|
|
|
305
311
|
Add a computed column that applies the model `whisper-1` to an existing Pixeltable column `tbl.audio`
|
|
306
312
|
of the table `tbl`:
|
|
307
313
|
|
|
308
|
-
>>> tbl
|
|
314
|
+
>>> tbl.add_computed_column(translation=translations(tbl.audio, model='whisper-1', language='en'))
|
|
309
315
|
"""
|
|
310
316
|
file = pathlib.Path(audio)
|
|
311
|
-
translation =
|
|
312
|
-
file=file, model=model, prompt=_opt(prompt), temperature=_opt(temperature)
|
|
317
|
+
translation = await _openai_client().audio.translations.create(
|
|
318
|
+
file=file, model=model, prompt=_opt(prompt), temperature=_opt(temperature), timeout=_opt(timeout)
|
|
313
319
|
)
|
|
314
320
|
return translation.dict()
|
|
315
321
|
|
|
@@ -318,17 +324,24 @@ def translations(
|
|
|
318
324
|
# Chat Endpoints
|
|
319
325
|
|
|
320
326
|
|
|
327
|
+
def _default_max_tokens(model: str) -> int:
|
|
328
|
+
if model in ('o1', 'o3-mini'):
|
|
329
|
+
return 65536
|
|
330
|
+
else:
|
|
331
|
+
return 1024
|
|
332
|
+
|
|
333
|
+
|
|
321
334
|
def _chat_completions_get_request_resources(
|
|
322
|
-
|
|
335
|
+
messages: list, model: str, max_completion_tokens: Optional[int], max_tokens: Optional[int], n: Optional[int]
|
|
323
336
|
) -> dict[str, int]:
|
|
324
|
-
completion_tokens = n * max_tokens
|
|
337
|
+
completion_tokens = (n or 1) * (max_completion_tokens or max_tokens or _default_max_tokens(model))
|
|
325
338
|
|
|
326
339
|
num_tokens = 0.0
|
|
327
340
|
for message in messages:
|
|
328
341
|
num_tokens += 4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
|
|
329
342
|
for key, value in message.items():
|
|
330
343
|
num_tokens += len(value) / 4
|
|
331
|
-
if key ==
|
|
344
|
+
if key == 'name': # if there's a name, the role is omitted
|
|
332
345
|
num_tokens -= 1 # role is always required and always 1 token
|
|
333
346
|
num_tokens += 2 # every reply is primed with <im_start>assistant
|
|
334
347
|
return {'requests': 1, 'tokens': int(num_tokens) + completion_tokens}
|
|
@@ -343,17 +356,20 @@ async def chat_completions(
|
|
|
343
356
|
logit_bias: Optional[dict[str, int]] = None,
|
|
344
357
|
logprobs: Optional[bool] = None,
|
|
345
358
|
top_logprobs: Optional[int] = None,
|
|
346
|
-
|
|
347
|
-
|
|
359
|
+
max_completion_tokens: Optional[int] = None,
|
|
360
|
+
max_tokens: Optional[int] = None,
|
|
361
|
+
n: Optional[int] = None,
|
|
348
362
|
presence_penalty: Optional[float] = None,
|
|
363
|
+
reasoning_effort: Optional[Literal['low', 'medium', 'high']] = None,
|
|
349
364
|
response_format: Optional[dict] = None,
|
|
350
365
|
seed: Optional[int] = None,
|
|
351
366
|
stop: Optional[list[str]] = None,
|
|
352
367
|
temperature: Optional[float] = None,
|
|
353
|
-
top_p: Optional[float] = None,
|
|
354
368
|
tools: Optional[list[dict]] = None,
|
|
355
369
|
tool_choice: Optional[dict] = None,
|
|
370
|
+
top_p: Optional[float] = None,
|
|
356
371
|
user: Optional[str] = None,
|
|
372
|
+
timeout: Optional[float] = None,
|
|
357
373
|
) -> dict:
|
|
358
374
|
"""
|
|
359
375
|
Creates a model response for the given chat conversation.
|
|
@@ -361,6 +377,10 @@ async def chat_completions(
|
|
|
361
377
|
Equivalent to the OpenAI `chat/completions` API endpoint.
|
|
362
378
|
For additional details, see: [https://platform.openai.com/docs/guides/chat-completions](https://platform.openai.com/docs/guides/chat-completions)
|
|
363
379
|
|
|
380
|
+
Request throttling:
|
|
381
|
+
Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
|
|
382
|
+
request and token capacity. No configuration is necessary.
|
|
383
|
+
|
|
364
384
|
__Requirements:__
|
|
365
385
|
|
|
366
386
|
- `pip install openai`
|
|
@@ -382,16 +402,10 @@ async def chat_completions(
|
|
|
382
402
|
{'role': 'system', 'content': 'You are a helpful assistant.'},
|
|
383
403
|
{'role': 'user', 'content': tbl.prompt}
|
|
384
404
|
]
|
|
385
|
-
tbl
|
|
405
|
+
tbl.add_computed_column(response=chat_completions(messages, model='gpt-4o-mini'))
|
|
386
406
|
"""
|
|
387
407
|
if tools is not None:
|
|
388
|
-
tools = [
|
|
389
|
-
{
|
|
390
|
-
'type': 'function',
|
|
391
|
-
'function': tool
|
|
392
|
-
}
|
|
393
|
-
for tool in tools
|
|
394
|
-
]
|
|
408
|
+
tools = [{'type': 'function', 'function': tool} for tool in tools]
|
|
395
409
|
|
|
396
410
|
tool_choice_: Union[str, dict, None] = None
|
|
397
411
|
if tool_choice is not None:
|
|
@@ -401,40 +415,43 @@ async def chat_completions(
|
|
|
401
415
|
tool_choice_ = 'required'
|
|
402
416
|
else:
|
|
403
417
|
assert tool_choice['tool'] is not None
|
|
404
|
-
tool_choice_ = {
|
|
405
|
-
'type': 'function',
|
|
406
|
-
'function': {'name': tool_choice['tool']}
|
|
407
|
-
}
|
|
418
|
+
tool_choice_ = {'type': 'function', 'function': {'name': tool_choice['tool']}}
|
|
408
419
|
|
|
409
420
|
extra_body: Optional[dict[str, Any]] = None
|
|
410
421
|
if tool_choice is not None and not tool_choice['parallel_tool_calls']:
|
|
411
422
|
extra_body = {'parallel_tool_calls': False}
|
|
412
423
|
|
|
413
424
|
# make sure the pool info exists prior to making the request
|
|
414
|
-
resource_pool =
|
|
425
|
+
resource_pool = _rate_limits_pool(model)
|
|
415
426
|
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
416
|
-
resource_pool, lambda: OpenAIRateLimitsInfo(_chat_completions_get_request_resources)
|
|
427
|
+
resource_pool, lambda: OpenAIRateLimitsInfo(_chat_completions_get_request_resources)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if max_completion_tokens is None and max_tokens is None:
|
|
431
|
+
max_completion_tokens = _default_max_tokens(model)
|
|
417
432
|
|
|
418
433
|
# cast(Any, ...): avoid mypy errors
|
|
419
|
-
result = await
|
|
434
|
+
result = await _openai_client().chat.completions.with_raw_response.create(
|
|
420
435
|
messages=messages,
|
|
421
436
|
model=model,
|
|
422
437
|
frequency_penalty=_opt(frequency_penalty),
|
|
423
438
|
logit_bias=_opt(logit_bias),
|
|
424
439
|
logprobs=_opt(logprobs),
|
|
425
440
|
top_logprobs=_opt(top_logprobs),
|
|
441
|
+
max_completion_tokens=_opt(max_completion_tokens),
|
|
426
442
|
max_tokens=_opt(max_tokens),
|
|
427
443
|
n=_opt(n),
|
|
428
444
|
presence_penalty=_opt(presence_penalty),
|
|
445
|
+
reasoning_effort=_opt(reasoning_effort),
|
|
429
446
|
response_format=_opt(cast(Any, response_format)),
|
|
430
447
|
seed=_opt(seed),
|
|
431
448
|
stop=_opt(stop),
|
|
432
449
|
temperature=_opt(temperature),
|
|
433
|
-
top_p=_opt(top_p),
|
|
434
450
|
tools=_opt(cast(Any, tools)),
|
|
435
451
|
tool_choice=_opt(cast(Any, tool_choice_)),
|
|
452
|
+
top_p=_opt(top_p),
|
|
436
453
|
user=_opt(user),
|
|
437
|
-
timeout=
|
|
454
|
+
timeout=_opt(timeout),
|
|
438
455
|
extra_body=extra_body,
|
|
439
456
|
)
|
|
440
457
|
|
|
@@ -444,14 +461,61 @@ async def chat_completions(
|
|
|
444
461
|
return json.loads(result.text)
|
|
445
462
|
|
|
446
463
|
|
|
464
|
+
def _vision_get_request_resources(
|
|
465
|
+
prompt: str,
|
|
466
|
+
image: PIL.Image.Image,
|
|
467
|
+
model: str,
|
|
468
|
+
max_completion_tokens: Optional[int],
|
|
469
|
+
max_tokens: Optional[int],
|
|
470
|
+
n: Optional[int],
|
|
471
|
+
) -> dict[str, int]:
|
|
472
|
+
completion_tokens = (n or 1) * (max_completion_tokens or max_tokens or _default_max_tokens(model))
|
|
473
|
+
prompt_tokens = len(prompt) / 4
|
|
474
|
+
|
|
475
|
+
# calculate image tokens based on
|
|
476
|
+
# https://platform.openai.com/docs/guides/vision/calculating-costs#calculating-costs
|
|
477
|
+
# assuming detail='high' (which appears to be the default, according to community forum posts)
|
|
478
|
+
|
|
479
|
+
# number of 512x512 crops; ceil(): partial crops still count as full crops
|
|
480
|
+
crops_width = math.ceil(image.width / 512)
|
|
481
|
+
crops_height = math.ceil(image.height / 512)
|
|
482
|
+
total_crops = crops_width * crops_height
|
|
483
|
+
|
|
484
|
+
BASE_TOKENS = 85 # base cost for the initial 512x512 overview
|
|
485
|
+
CROP_TOKENS = 170 # cost per additional 512x512 crop
|
|
486
|
+
img_tokens = BASE_TOKENS + (CROP_TOKENS * total_crops)
|
|
487
|
+
|
|
488
|
+
total_tokens = (
|
|
489
|
+
prompt_tokens
|
|
490
|
+
+ img_tokens
|
|
491
|
+
+ completion_tokens
|
|
492
|
+
+ 4 # for <im_start>{role/name}\n{content}<im_end>\n
|
|
493
|
+
+ 2 # for reply's <im_start>assistant
|
|
494
|
+
)
|
|
495
|
+
return {'requests': 1, 'tokens': int(total_tokens)}
|
|
496
|
+
|
|
497
|
+
|
|
447
498
|
@pxt.udf
|
|
448
|
-
def vision(
|
|
499
|
+
async def vision(
|
|
500
|
+
prompt: str,
|
|
501
|
+
image: PIL.Image.Image,
|
|
502
|
+
*,
|
|
503
|
+
model: str,
|
|
504
|
+
max_completion_tokens: Optional[int] = None,
|
|
505
|
+
max_tokens: Optional[int] = None,
|
|
506
|
+
n: Optional[int] = 1,
|
|
507
|
+
timeout: Optional[float] = None,
|
|
508
|
+
) -> str:
|
|
449
509
|
"""
|
|
450
510
|
Analyzes an image with the OpenAI vision capability. This is a convenience function that takes an image and
|
|
451
511
|
prompt, and constructs a chat completion request that utilizes OpenAI vision.
|
|
452
512
|
|
|
453
513
|
For additional details, see: [https://platform.openai.com/docs/guides/vision](https://platform.openai.com/docs/guides/vision)
|
|
454
514
|
|
|
515
|
+
Request throttling:
|
|
516
|
+
Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
|
|
517
|
+
request and token capacity. No configuration is necessary.
|
|
518
|
+
|
|
455
519
|
__Requirements:__
|
|
456
520
|
|
|
457
521
|
- `pip install openai`
|
|
@@ -468,7 +532,7 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
|
|
|
468
532
|
Add a computed column that applies the model `gpt-4o-mini` to an existing Pixeltable column `tbl.image`
|
|
469
533
|
of the table `tbl`:
|
|
470
534
|
|
|
471
|
-
>>> tbl
|
|
535
|
+
>>> tbl.add_computed_column(response=vision("What's in this image?", tbl.image, model='gpt-4o-mini'))
|
|
472
536
|
"""
|
|
473
537
|
# TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
|
|
474
538
|
bytes_arr = io.BytesIO()
|
|
@@ -484,8 +548,30 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
|
|
|
484
548
|
],
|
|
485
549
|
}
|
|
486
550
|
]
|
|
487
|
-
|
|
488
|
-
|
|
551
|
+
|
|
552
|
+
# make sure the pool info exists prior to making the request
|
|
553
|
+
resource_pool = _rate_limits_pool(model)
|
|
554
|
+
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
555
|
+
resource_pool, lambda: OpenAIRateLimitsInfo(_vision_get_request_resources)
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if max_completion_tokens is None and max_tokens is None:
|
|
559
|
+
max_completion_tokens = _default_max_tokens(model)
|
|
560
|
+
|
|
561
|
+
result = await _openai_client().chat.completions.with_raw_response.create(
|
|
562
|
+
messages=messages, # type: ignore
|
|
563
|
+
model=model,
|
|
564
|
+
max_completion_tokens=_opt(max_completion_tokens),
|
|
565
|
+
max_tokens=_opt(max_tokens),
|
|
566
|
+
n=_opt(n),
|
|
567
|
+
timeout=_opt(timeout),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
requests_info, tokens_info = _get_header_info(result.headers)
|
|
571
|
+
rate_limits_info.record(requests=requests_info, tokens=tokens_info)
|
|
572
|
+
|
|
573
|
+
result = json.loads(result.text)
|
|
574
|
+
return result['choices'][0]['message']['content']
|
|
489
575
|
|
|
490
576
|
|
|
491
577
|
#####################################
|
|
@@ -505,7 +591,12 @@ def _embeddings_get_request_resources(input: list[str]) -> dict[str, int]:
|
|
|
505
591
|
|
|
506
592
|
@pxt.udf(batch_size=32)
|
|
507
593
|
async def embeddings(
|
|
508
|
-
input: Batch[str],
|
|
594
|
+
input: Batch[str],
|
|
595
|
+
*,
|
|
596
|
+
model: str,
|
|
597
|
+
dimensions: Optional[int] = None,
|
|
598
|
+
user: Optional[str] = None,
|
|
599
|
+
timeout: Optional[float] = None,
|
|
509
600
|
) -> Batch[pxt.Array[(None,), pxt.Float]]:
|
|
510
601
|
"""
|
|
511
602
|
Creates an embedding vector representing the input text.
|
|
@@ -513,6 +604,10 @@ async def embeddings(
|
|
|
513
604
|
Equivalent to the OpenAI `embeddings` API endpoint.
|
|
514
605
|
For additional details, see: [https://platform.openai.com/docs/guides/embeddings](https://platform.openai.com/docs/guides/embeddings)
|
|
515
606
|
|
|
607
|
+
Request throttling:
|
|
608
|
+
Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
|
|
609
|
+
request and token capacity. No configuration is necessary.
|
|
610
|
+
|
|
516
611
|
__Requirements:__
|
|
517
612
|
|
|
518
613
|
- `pip install openai`
|
|
@@ -532,14 +627,24 @@ async def embeddings(
|
|
|
532
627
|
Add a computed column that applies the model `text-embedding-3-small` to an existing
|
|
533
628
|
Pixeltable column `tbl.text` of the table `tbl`:
|
|
534
629
|
|
|
535
|
-
>>> tbl
|
|
630
|
+
>>> tbl.add_computed_column(embed=embeddings(tbl.text, model='text-embedding-3-small'))
|
|
631
|
+
|
|
632
|
+
Add an embedding index to an existing column `text`, using the model `text-embedding-3-small`:
|
|
633
|
+
|
|
634
|
+
>>> tbl.add_embedding_index(embedding=embeddings.using(model='text-embedding-3-small'))
|
|
536
635
|
"""
|
|
537
636
|
_logger.debug(f'embeddings: batch_size={len(input)}')
|
|
538
|
-
resource_pool =
|
|
637
|
+
resource_pool = _rate_limits_pool(model)
|
|
539
638
|
rate_limits_info = env.Env.get().get_resource_pool_info(
|
|
540
|
-
resource_pool, lambda: OpenAIRateLimitsInfo(_embeddings_get_request_resources)
|
|
541
|
-
|
|
542
|
-
|
|
639
|
+
resource_pool, lambda: OpenAIRateLimitsInfo(_embeddings_get_request_resources)
|
|
640
|
+
)
|
|
641
|
+
result = await _openai_client().embeddings.with_raw_response.create(
|
|
642
|
+
input=input,
|
|
643
|
+
model=model,
|
|
644
|
+
dimensions=_opt(dimensions),
|
|
645
|
+
user=_opt(user),
|
|
646
|
+
encoding_format='float',
|
|
647
|
+
timeout=_opt(timeout),
|
|
543
648
|
)
|
|
544
649
|
requests_info, tokens_info = _get_header_info(result.headers)
|
|
545
650
|
rate_limits_info.record(requests=requests_info, tokens=tokens_info)
|
|
@@ -561,7 +666,7 @@ def _(model: str, dimensions: Optional[int] = None) -> pxt.ArrayType:
|
|
|
561
666
|
|
|
562
667
|
|
|
563
668
|
@pxt.udf
|
|
564
|
-
def image_generations(
|
|
669
|
+
async def image_generations(
|
|
565
670
|
prompt: str,
|
|
566
671
|
*,
|
|
567
672
|
model: str = 'dall-e-2',
|
|
@@ -569,6 +674,7 @@ def image_generations(
|
|
|
569
674
|
size: Optional[str] = None,
|
|
570
675
|
style: Optional[str] = None,
|
|
571
676
|
user: Optional[str] = None,
|
|
677
|
+
timeout: Optional[float] = None,
|
|
572
678
|
) -> PIL.Image.Image:
|
|
573
679
|
"""
|
|
574
680
|
Creates an image given a prompt.
|
|
@@ -576,6 +682,10 @@ def image_generations(
|
|
|
576
682
|
Equivalent to the OpenAI `images/generations` API endpoint.
|
|
577
683
|
For additional details, see: [https://platform.openai.com/docs/guides/images](https://platform.openai.com/docs/guides/images)
|
|
578
684
|
|
|
685
|
+
Request throttling:
|
|
686
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
687
|
+
limit is configured, uses a default of 600 RPM.
|
|
688
|
+
|
|
579
689
|
__Requirements:__
|
|
580
690
|
|
|
581
691
|
- `pip install openai`
|
|
@@ -593,17 +703,18 @@ def image_generations(
|
|
|
593
703
|
Add a computed column that applies the model `dall-e-2` to an existing
|
|
594
704
|
Pixeltable column `tbl.text` of the table `tbl`:
|
|
595
705
|
|
|
596
|
-
>>> tbl
|
|
706
|
+
>>> tbl.add_computed_column(gen_image=image_generations(tbl.text, model='dall-e-2'))
|
|
597
707
|
"""
|
|
598
708
|
# TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
|
|
599
|
-
result =
|
|
709
|
+
result = await _openai_client().images.generate(
|
|
600
710
|
prompt=prompt,
|
|
601
711
|
model=_opt(model),
|
|
602
|
-
quality=_opt(quality),
|
|
603
|
-
size=_opt(size),
|
|
604
|
-
style=_opt(style),
|
|
712
|
+
quality=_opt(quality), # type: ignore
|
|
713
|
+
size=_opt(size), # type: ignore
|
|
714
|
+
style=_opt(style), # type: ignore
|
|
605
715
|
user=_opt(user),
|
|
606
716
|
response_format='b64_json',
|
|
717
|
+
timeout=_opt(timeout),
|
|
607
718
|
)
|
|
608
719
|
b64_str = result.data[0].b64_json
|
|
609
720
|
b64_bytes = base64.b64decode(b64_str)
|
|
@@ -620,7 +731,7 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
|
|
|
620
731
|
if x_pos == -1:
|
|
621
732
|
return pxt.ImageType()
|
|
622
733
|
try:
|
|
623
|
-
width, height = int(size[:x_pos]), int(size[x_pos + 1:])
|
|
734
|
+
width, height = int(size[:x_pos]), int(size[x_pos + 1 :])
|
|
624
735
|
except ValueError:
|
|
625
736
|
return pxt.ImageType()
|
|
626
737
|
return pxt.ImageType(size=(width, height))
|
|
@@ -631,13 +742,17 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
|
|
|
631
742
|
|
|
632
743
|
|
|
633
744
|
@pxt.udf
|
|
634
|
-
def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
745
|
+
async def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
635
746
|
"""
|
|
636
747
|
Classifies if text is potentially harmful.
|
|
637
748
|
|
|
638
749
|
Equivalent to the OpenAI `moderations` API endpoint.
|
|
639
750
|
For additional details, see: [https://platform.openai.com/docs/guides/moderation](https://platform.openai.com/docs/guides/moderation)
|
|
640
751
|
|
|
752
|
+
Request throttling:
|
|
753
|
+
Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
|
|
754
|
+
limit is configured, uses a default of 600 RPM.
|
|
755
|
+
|
|
641
756
|
__Requirements:__
|
|
642
757
|
|
|
643
758
|
- `pip install openai`
|
|
@@ -655,22 +770,26 @@ def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
|
|
|
655
770
|
Add a computed column that applies the model `text-moderation-stable` to an existing
|
|
656
771
|
Pixeltable column `tbl.input` of the table `tbl`:
|
|
657
772
|
|
|
658
|
-
>>> tbl
|
|
773
|
+
>>> tbl.add_computed_column(moderations=moderations(tbl.text, model='text-moderation-stable'))
|
|
659
774
|
"""
|
|
660
|
-
result =
|
|
775
|
+
result = await _openai_client().moderations.create(input=input, model=_opt(model))
|
|
661
776
|
return result.dict()
|
|
662
777
|
|
|
663
778
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
779
|
+
@speech.resource_pool
|
|
780
|
+
@transcriptions.resource_pool
|
|
781
|
+
@translations.resource_pool
|
|
782
|
+
@image_generations.resource_pool
|
|
783
|
+
@moderations.resource_pool
|
|
784
|
+
def _(model: str) -> str:
|
|
785
|
+
return f'request-rate:openai:{model}'
|
|
786
|
+
|
|
787
|
+
|
|
667
788
|
@chat_completions.resource_pool
|
|
668
|
-
|
|
789
|
+
@vision.resource_pool
|
|
669
790
|
@embeddings.resource_pool
|
|
670
|
-
# @image_generations.resource_pool
|
|
671
|
-
# @moderations.resource_pool
|
|
672
791
|
def _(model: str) -> str:
|
|
673
|
-
return
|
|
792
|
+
return _rate_limits_pool(model)
|
|
674
793
|
|
|
675
794
|
|
|
676
795
|
def invoke_tools(tools: Tools, response: exprs.Expr) -> exprs.InlineDict:
|
|
@@ -684,9 +803,7 @@ def _openai_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
|
|
|
684
803
|
return None
|
|
685
804
|
openai_tool_calls = response['choices'][0]['message']['tool_calls']
|
|
686
805
|
return {
|
|
687
|
-
tool_call['function']['name']: {
|
|
688
|
-
'args': json.loads(tool_call['function']['arguments'])
|
|
689
|
-
}
|
|
806
|
+
tool_call['function']['name']: {'args': json.loads(tool_call['function']['arguments'])}
|
|
690
807
|
for tool_call in openai_tool_calls
|
|
691
808
|
}
|
|
692
809
|
|
|
@@ -696,6 +813,7 @@ _T = TypeVar('_T')
|
|
|
696
813
|
|
|
697
814
|
def _opt(arg: _T) -> Union[_T, 'openai.NotGiven']:
|
|
698
815
|
import openai
|
|
816
|
+
|
|
699
817
|
return arg if arg is not None else openai.NOT_GIVEN
|
|
700
818
|
|
|
701
819
|
|