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.

Files changed (150) hide show
  1. pixeltable/__init__.py +64 -11
  2. pixeltable/__version__.py +2 -2
  3. pixeltable/catalog/__init__.py +1 -1
  4. pixeltable/catalog/catalog.py +50 -27
  5. pixeltable/catalog/column.py +27 -11
  6. pixeltable/catalog/dir.py +6 -4
  7. pixeltable/catalog/globals.py +8 -1
  8. pixeltable/catalog/insertable_table.py +22 -12
  9. pixeltable/catalog/named_function.py +10 -6
  10. pixeltable/catalog/path.py +3 -2
  11. pixeltable/catalog/path_dict.py +8 -6
  12. pixeltable/catalog/schema_object.py +2 -1
  13. pixeltable/catalog/table.py +121 -101
  14. pixeltable/catalog/table_version.py +291 -142
  15. pixeltable/catalog/table_version_path.py +8 -5
  16. pixeltable/catalog/view.py +67 -26
  17. pixeltable/dataframe.py +106 -81
  18. pixeltable/env.py +28 -24
  19. pixeltable/exec/__init__.py +2 -2
  20. pixeltable/exec/aggregation_node.py +10 -4
  21. pixeltable/exec/cache_prefetch_node.py +5 -3
  22. pixeltable/exec/component_iteration_node.py +9 -9
  23. pixeltable/exec/data_row_batch.py +21 -10
  24. pixeltable/exec/exec_context.py +10 -3
  25. pixeltable/exec/exec_node.py +23 -12
  26. pixeltable/exec/expr_eval/evaluators.py +13 -7
  27. pixeltable/exec/expr_eval/expr_eval_node.py +24 -15
  28. pixeltable/exec/expr_eval/globals.py +30 -7
  29. pixeltable/exec/expr_eval/row_buffer.py +5 -6
  30. pixeltable/exec/expr_eval/schedulers.py +151 -31
  31. pixeltable/exec/in_memory_data_node.py +8 -7
  32. pixeltable/exec/row_update_node.py +15 -5
  33. pixeltable/exec/sql_node.py +56 -27
  34. pixeltable/exprs/__init__.py +2 -2
  35. pixeltable/exprs/arithmetic_expr.py +57 -26
  36. pixeltable/exprs/array_slice.py +1 -1
  37. pixeltable/exprs/column_property_ref.py +2 -1
  38. pixeltable/exprs/column_ref.py +20 -15
  39. pixeltable/exprs/comparison.py +6 -2
  40. pixeltable/exprs/compound_predicate.py +1 -3
  41. pixeltable/exprs/data_row.py +2 -2
  42. pixeltable/exprs/expr.py +108 -72
  43. pixeltable/exprs/expr_dict.py +2 -1
  44. pixeltable/exprs/expr_set.py +3 -1
  45. pixeltable/exprs/function_call.py +39 -41
  46. pixeltable/exprs/globals.py +1 -0
  47. pixeltable/exprs/in_predicate.py +2 -2
  48. pixeltable/exprs/inline_expr.py +20 -17
  49. pixeltable/exprs/json_mapper.py +4 -2
  50. pixeltable/exprs/json_path.py +12 -18
  51. pixeltable/exprs/literal.py +5 -9
  52. pixeltable/exprs/method_ref.py +1 -0
  53. pixeltable/exprs/object_ref.py +1 -1
  54. pixeltable/exprs/row_builder.py +32 -17
  55. pixeltable/exprs/rowid_ref.py +14 -5
  56. pixeltable/exprs/similarity_expr.py +11 -6
  57. pixeltable/exprs/sql_element_cache.py +1 -1
  58. pixeltable/exprs/type_cast.py +24 -9
  59. pixeltable/ext/__init__.py +1 -0
  60. pixeltable/ext/functions/__init__.py +1 -0
  61. pixeltable/ext/functions/whisperx.py +2 -2
  62. pixeltable/ext/functions/yolox.py +11 -11
  63. pixeltable/func/aggregate_function.py +17 -13
  64. pixeltable/func/callable_function.py +6 -6
  65. pixeltable/func/expr_template_function.py +15 -14
  66. pixeltable/func/function.py +16 -16
  67. pixeltable/func/function_registry.py +11 -8
  68. pixeltable/func/globals.py +4 -2
  69. pixeltable/func/query_template_function.py +12 -13
  70. pixeltable/func/signature.py +18 -9
  71. pixeltable/func/tools.py +10 -17
  72. pixeltable/func/udf.py +106 -11
  73. pixeltable/functions/__init__.py +21 -2
  74. pixeltable/functions/anthropic.py +16 -12
  75. pixeltable/functions/fireworks.py +63 -5
  76. pixeltable/functions/gemini.py +13 -3
  77. pixeltable/functions/globals.py +18 -6
  78. pixeltable/functions/huggingface.py +20 -38
  79. pixeltable/functions/image.py +7 -3
  80. pixeltable/functions/json.py +1 -0
  81. pixeltable/functions/llama_cpp.py +1 -4
  82. pixeltable/functions/mistralai.py +31 -20
  83. pixeltable/functions/ollama.py +4 -18
  84. pixeltable/functions/openai.py +231 -113
  85. pixeltable/functions/replicate.py +11 -10
  86. pixeltable/functions/string.py +70 -7
  87. pixeltable/functions/timestamp.py +21 -8
  88. pixeltable/functions/together.py +66 -52
  89. pixeltable/functions/video.py +1 -0
  90. pixeltable/functions/vision.py +14 -11
  91. pixeltable/functions/whisper.py +2 -1
  92. pixeltable/globals.py +60 -26
  93. pixeltable/index/__init__.py +1 -1
  94. pixeltable/index/btree.py +5 -3
  95. pixeltable/index/embedding_index.py +15 -14
  96. pixeltable/io/__init__.py +1 -1
  97. pixeltable/io/external_store.py +30 -25
  98. pixeltable/io/fiftyone.py +6 -14
  99. pixeltable/io/globals.py +33 -27
  100. pixeltable/io/hf_datasets.py +2 -1
  101. pixeltable/io/label_studio.py +77 -68
  102. pixeltable/io/pandas.py +36 -23
  103. pixeltable/io/parquet.py +9 -12
  104. pixeltable/iterators/__init__.py +1 -0
  105. pixeltable/iterators/audio.py +205 -0
  106. pixeltable/iterators/document.py +19 -8
  107. pixeltable/iterators/image.py +6 -24
  108. pixeltable/iterators/string.py +3 -6
  109. pixeltable/iterators/video.py +1 -7
  110. pixeltable/metadata/__init__.py +7 -1
  111. pixeltable/metadata/converters/convert_10.py +2 -2
  112. pixeltable/metadata/converters/convert_15.py +1 -5
  113. pixeltable/metadata/converters/convert_16.py +2 -4
  114. pixeltable/metadata/converters/convert_17.py +2 -4
  115. pixeltable/metadata/converters/convert_18.py +2 -4
  116. pixeltable/metadata/converters/convert_19.py +2 -5
  117. pixeltable/metadata/converters/convert_20.py +1 -4
  118. pixeltable/metadata/converters/convert_21.py +4 -6
  119. pixeltable/metadata/converters/convert_22.py +1 -0
  120. pixeltable/metadata/converters/convert_23.py +5 -5
  121. pixeltable/metadata/converters/convert_24.py +12 -13
  122. pixeltable/metadata/converters/convert_26.py +23 -0
  123. pixeltable/metadata/converters/util.py +3 -4
  124. pixeltable/metadata/notes.py +1 -0
  125. pixeltable/metadata/schema.py +13 -2
  126. pixeltable/plan.py +173 -98
  127. pixeltable/share/__init__.py +0 -0
  128. pixeltable/share/packager.py +218 -0
  129. pixeltable/store.py +42 -26
  130. pixeltable/type_system.py +102 -75
  131. pixeltable/utils/arrow.py +7 -8
  132. pixeltable/utils/coco.py +16 -17
  133. pixeltable/utils/code.py +1 -1
  134. pixeltable/utils/console_output.py +6 -3
  135. pixeltable/utils/description_helper.py +7 -7
  136. pixeltable/utils/documents.py +3 -1
  137. pixeltable/utils/filecache.py +12 -7
  138. pixeltable/utils/http_server.py +9 -8
  139. pixeltable/utils/iceberg.py +14 -0
  140. pixeltable/utils/media_store.py +3 -2
  141. pixeltable/utils/pytorch.py +11 -14
  142. pixeltable/utils/s3.py +1 -0
  143. pixeltable/utils/sql.py +1 -0
  144. pixeltable/utils/transactional_directory.py +2 -2
  145. {pixeltable-0.3.2.dist-info → pixeltable-0.3.4.dist-info}/METADATA +9 -9
  146. pixeltable-0.3.4.dist-info/RECORD +166 -0
  147. pixeltable-0.3.2.dist-info/RECORD +0 -161
  148. {pixeltable-0.3.2.dist-info → pixeltable-0.3.4.dist-info}/LICENSE +0 -0
  149. {pixeltable-0.3.2.dist-info → pixeltable-0.3.4.dist-info}/WHEEL +0 -0
  150. {pixeltable-0.3.2.dist-info → pixeltable-0.3.4.dist-info}/entry_points.txt +0 -0
@@ -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, Any, Type
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 tenacity
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) -> tuple['openai.OpenAI', 'openai.AsyncOpenAI']:
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
- def _async_openai_client() -> 'openai.AsyncOpenAI':
52
- return env.Env.get().get_client('openai')[1]
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
- # Exponential backoff decorator using tenacity.
56
- # TODO(aaron-siegel): Right now this hardwires random exponential backoff with defaults suggested
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 _resource_pool(model: str) -> str:
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 potentially a bug in AsyncConnectionPool)
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("Invalid duration format")
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
- headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
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
- input: str, *, model: str, voice: str, response_format: Optional[str] = None, speed: Optional[float] = None
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['audio'] = speech(tbl.text, model='tts-1', voice='nova')
211
+ >>> tbl.add_computed_column(audio=speech(tbl.text, model='tts-1', voice='nova'))
226
212
  """
227
- content = _retry(_openai_client().audio.speech.create)(
228
- input=input, model=model, voice=voice, response_format=_opt(response_format), speed=_opt(speed)
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['transcription'] = transcriptions(tbl.audio, model='whisper-1', language='en')
264
+ >>> tbl.add_computed_column(transcription=transcriptions(tbl.audio, model='whisper-1', language='en'))
269
265
  """
270
266
  file = pathlib.Path(audio)
271
- transcription = _retry(_openai_client().audio.transcriptions.create)(
272
- file=file, model=model, language=_opt(language), prompt=_opt(prompt), temperature=_opt(temperature)
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['translation'] = translations(tbl.audio, model='whisper-1', language='en')
314
+ >>> tbl.add_computed_column(translation=translations(tbl.audio, model='whisper-1', language='en'))
309
315
  """
310
316
  file = pathlib.Path(audio)
311
- translation = _retry(_openai_client().audio.translations.create)(
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
- messages: list, max_tokens: Optional[int], n: Optional[int]
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 == "name": # if there's a name, the role is omitted
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
- max_tokens: Optional[int] = 1024,
347
- n: Optional[int] = 1,
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['response'] = chat_completions(messages, model='gpt-4o-mini')
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 = _resource_pool(model)
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 _async_openai_client().chat.completions.with_raw_response.create(
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=10,
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(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
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['response'] = vision("What's in this image?", tbl.image, model='gpt-4o-mini')
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
- result = _retry(_openai_client().chat.completions.create)(messages=messages, model=model)
488
- return result.choices[0].message.content
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], *, model: str, dimensions: Optional[int] = None, user: Optional[str] = None
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['embed'] = embeddings(tbl.text, model='text-embedding-3-small')
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 = _resource_pool(model)
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
- result = await _async_openai_client().embeddings.with_raw_response.create(
542
- input=input, model=model, dimensions=_opt(dimensions), user=_opt(user), encoding_format='float'
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['gen_image'] = image_generations(tbl.text, model='dall-e-2')
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 = _retry(_openai_client().images.generate)(
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['moderations'] = moderations(tbl.text, model='text-moderation-stable')
773
+ >>> tbl.add_computed_column(moderations=moderations(tbl.text, model='text-moderation-stable'))
659
774
  """
660
- result = _retry(_openai_client().moderations.create)(input=input, model=_opt(model))
775
+ result = await _openai_client().moderations.create(input=input, model=_opt(model))
661
776
  return result.dict()
662
777
 
663
778
 
664
- # @speech.resource_pool
665
- # @transcriptions.resource_pool
666
- # @translations.resource_pool
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
- # @vision.resource_pool
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 _resource_pool(model)
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