pixeltable 0.3.2__py3-none-any.whl → 0.3.3__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 (147) 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 +102 -72
  18. pixeltable/env.py +20 -21
  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 -8
  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 +101 -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 +201 -108
  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 +33 -9
  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/store.py +42 -26
  128. pixeltable/type_system.py +62 -54
  129. pixeltable/utils/arrow.py +1 -2
  130. pixeltable/utils/coco.py +16 -17
  131. pixeltable/utils/code.py +1 -1
  132. pixeltable/utils/console_output.py +6 -3
  133. pixeltable/utils/description_helper.py +7 -7
  134. pixeltable/utils/documents.py +3 -1
  135. pixeltable/utils/filecache.py +12 -7
  136. pixeltable/utils/http_server.py +9 -8
  137. pixeltable/utils/media_store.py +2 -1
  138. pixeltable/utils/pytorch.py +11 -14
  139. pixeltable/utils/s3.py +1 -0
  140. pixeltable/utils/sql.py +1 -0
  141. pixeltable/utils/transactional_directory.py +2 -2
  142. {pixeltable-0.3.2.dist-info → pixeltable-0.3.3.dist-info}/METADATA +6 -8
  143. pixeltable-0.3.3.dist-info/RECORD +163 -0
  144. pixeltable-0.3.2.dist-info/RECORD +0 -161
  145. {pixeltable-0.3.2.dist-info → pixeltable-0.3.3.dist-info}/LICENSE +0 -0
  146. {pixeltable-0.3.2.dist-info → pixeltable-0.3.3.dist-info}/WHEEL +0 -0
  147. {pixeltable-0.3.2.dist-info → pixeltable-0.3.3.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, 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
37
 
50
-
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
 
@@ -319,7 +325,7 @@ def translations(
319
325
 
320
326
 
321
327
  def _chat_completions_get_request_resources(
322
- messages: list, max_tokens: Optional[int], n: Optional[int]
328
+ messages: list, max_tokens: Optional[int], n: Optional[int]
323
329
  ) -> dict[str, int]:
324
330
  completion_tokens = n * max_tokens
325
331
 
@@ -328,7 +334,7 @@ def _chat_completions_get_request_resources(
328
334
  num_tokens += 4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
329
335
  for key, value in message.items():
330
336
  num_tokens += len(value) / 4
331
- if key == "name": # if there's a name, the role is omitted
337
+ if key == 'name': # if there's a name, the role is omitted
332
338
  num_tokens -= 1 # role is always required and always 1 token
333
339
  num_tokens += 2 # every reply is primed with <im_start>assistant
334
340
  return {'requests': 1, 'tokens': int(num_tokens) + completion_tokens}
@@ -354,6 +360,7 @@ async def chat_completions(
354
360
  tools: Optional[list[dict]] = None,
355
361
  tool_choice: Optional[dict] = None,
356
362
  user: Optional[str] = None,
363
+ timeout: Optional[float] = None,
357
364
  ) -> dict:
358
365
  """
359
366
  Creates a model response for the given chat conversation.
@@ -361,6 +368,10 @@ async def chat_completions(
361
368
  Equivalent to the OpenAI `chat/completions` API endpoint.
362
369
  For additional details, see: [https://platform.openai.com/docs/guides/chat-completions](https://platform.openai.com/docs/guides/chat-completions)
363
370
 
371
+ Request throttling:
372
+ Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
373
+ request and token capacity. No configuration is necessary.
374
+
364
375
  __Requirements:__
365
376
 
366
377
  - `pip install openai`
@@ -382,16 +393,10 @@ async def chat_completions(
382
393
  {'role': 'system', 'content': 'You are a helpful assistant.'},
383
394
  {'role': 'user', 'content': tbl.prompt}
384
395
  ]
385
- tbl['response'] = chat_completions(messages, model='gpt-4o-mini')
396
+ tbl.add_computed_column(response=chat_completions(messages, model='gpt-4o-mini'))
386
397
  """
387
398
  if tools is not None:
388
- tools = [
389
- {
390
- 'type': 'function',
391
- 'function': tool
392
- }
393
- for tool in tools
394
- ]
399
+ tools = [{'type': 'function', 'function': tool} for tool in tools]
395
400
 
396
401
  tool_choice_: Union[str, dict, None] = None
397
402
  if tool_choice is not None:
@@ -401,22 +406,20 @@ async def chat_completions(
401
406
  tool_choice_ = 'required'
402
407
  else:
403
408
  assert tool_choice['tool'] is not None
404
- tool_choice_ = {
405
- 'type': 'function',
406
- 'function': {'name': tool_choice['tool']}
407
- }
409
+ tool_choice_ = {'type': 'function', 'function': {'name': tool_choice['tool']}}
408
410
 
409
411
  extra_body: Optional[dict[str, Any]] = None
410
412
  if tool_choice is not None and not tool_choice['parallel_tool_calls']:
411
413
  extra_body = {'parallel_tool_calls': False}
412
414
 
413
415
  # make sure the pool info exists prior to making the request
414
- resource_pool = _resource_pool(model)
416
+ resource_pool = _rate_limits_pool(model)
415
417
  rate_limits_info = env.Env.get().get_resource_pool_info(
416
- resource_pool, lambda: OpenAIRateLimitsInfo(_chat_completions_get_request_resources))
418
+ resource_pool, lambda: OpenAIRateLimitsInfo(_chat_completions_get_request_resources)
419
+ )
417
420
 
418
421
  # cast(Any, ...): avoid mypy errors
419
- result = await _async_openai_client().chat.completions.with_raw_response.create(
422
+ result = await _openai_client().chat.completions.with_raw_response.create(
420
423
  messages=messages,
421
424
  model=model,
422
425
  frequency_penalty=_opt(frequency_penalty),
@@ -434,7 +437,7 @@ async def chat_completions(
434
437
  tools=_opt(cast(Any, tools)),
435
438
  tool_choice=_opt(cast(Any, tool_choice_)),
436
439
  user=_opt(user),
437
- timeout=10,
440
+ timeout=_opt(timeout),
438
441
  extra_body=extra_body,
439
442
  )
440
443
 
@@ -444,14 +447,55 @@ async def chat_completions(
444
447
  return json.loads(result.text)
445
448
 
446
449
 
450
+ def _vision_get_request_resources(
451
+ prompt: str, image: PIL.Image.Image, max_tokens: Optional[int], n: Optional[int]
452
+ ) -> dict[str, int]:
453
+ completion_tokens = n * max_tokens
454
+ prompt_tokens = len(prompt) / 4
455
+
456
+ # calculate image tokens based on
457
+ # https://platform.openai.com/docs/guides/vision/calculating-costs#calculating-costs
458
+ # assuming detail='high' (which appears to be the default, according to community forum posts)
459
+
460
+ # number of 512x512 crops; ceil(): partial crops still count as full crops
461
+ crops_width = math.ceil(image.width / 512)
462
+ crops_height = math.ceil(image.height / 512)
463
+ total_crops = crops_width * crops_height
464
+
465
+ BASE_TOKENS = 85 # base cost for the initial 512x512 overview
466
+ CROP_TOKENS = 170 # cost per additional 512x512 crop
467
+ img_tokens = BASE_TOKENS + (CROP_TOKENS * total_crops)
468
+
469
+ total_tokens = (
470
+ prompt_tokens
471
+ + img_tokens
472
+ + completion_tokens
473
+ + 4 # for <im_start>{role/name}\n{content}<im_end>\n
474
+ + 2 # for reply's <im_start>assistant
475
+ )
476
+ return {'requests': 1, 'tokens': int(total_tokens)}
477
+
478
+
447
479
  @pxt.udf
448
- def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
480
+ async def vision(
481
+ prompt: str,
482
+ image: PIL.Image.Image,
483
+ *,
484
+ model: str,
485
+ max_tokens: Optional[int] = 1024,
486
+ n: Optional[int] = 1,
487
+ timeout: Optional[float] = None,
488
+ ) -> str:
449
489
  """
450
490
  Analyzes an image with the OpenAI vision capability. This is a convenience function that takes an image and
451
491
  prompt, and constructs a chat completion request that utilizes OpenAI vision.
452
492
 
453
493
  For additional details, see: [https://platform.openai.com/docs/guides/vision](https://platform.openai.com/docs/guides/vision)
454
494
 
495
+ Request throttling:
496
+ Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
497
+ request and token capacity. No configuration is necessary.
498
+
455
499
  __Requirements:__
456
500
 
457
501
  - `pip install openai`
@@ -468,7 +512,7 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
468
512
  Add a computed column that applies the model `gpt-4o-mini` to an existing Pixeltable column `tbl.image`
469
513
  of the table `tbl`:
470
514
 
471
- >>> tbl['response'] = vision("What's in this image?", tbl.image, model='gpt-4o-mini')
515
+ >>> tbl.add_computed_column(response=vision("What's in this image?", tbl.image, model='gpt-4o-mini'))
472
516
  """
473
517
  # TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
474
518
  bytes_arr = io.BytesIO()
@@ -484,8 +528,25 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
484
528
  ],
485
529
  }
486
530
  ]
487
- result = _retry(_openai_client().chat.completions.create)(messages=messages, model=model)
488
- return result.choices[0].message.content
531
+
532
+ # make sure the pool info exists prior to making the request
533
+ resource_pool = _rate_limits_pool(model)
534
+ rate_limits_info = env.Env.get().get_resource_pool_info(
535
+ resource_pool, lambda: OpenAIRateLimitsInfo(_vision_get_request_resources)
536
+ )
537
+ result = await _openai_client().chat.completions.with_raw_response.create(
538
+ messages=messages, # type: ignore
539
+ model=model,
540
+ max_tokens=_opt(max_tokens),
541
+ n=_opt(n),
542
+ timeout=_opt(timeout),
543
+ )
544
+
545
+ requests_info, tokens_info = _get_header_info(result.headers)
546
+ rate_limits_info.record(requests=requests_info, tokens=tokens_info)
547
+
548
+ result = json.loads(result.text)
549
+ return result['choices'][0]['message']['content']
489
550
 
490
551
 
491
552
  #####################################
@@ -505,7 +566,12 @@ def _embeddings_get_request_resources(input: list[str]) -> dict[str, int]:
505
566
 
506
567
  @pxt.udf(batch_size=32)
507
568
  async def embeddings(
508
- input: Batch[str], *, model: str, dimensions: Optional[int] = None, user: Optional[str] = None
569
+ input: Batch[str],
570
+ *,
571
+ model: str,
572
+ dimensions: Optional[int] = None,
573
+ user: Optional[str] = None,
574
+ timeout: Optional[float] = None,
509
575
  ) -> Batch[pxt.Array[(None,), pxt.Float]]:
510
576
  """
511
577
  Creates an embedding vector representing the input text.
@@ -513,6 +579,10 @@ async def embeddings(
513
579
  Equivalent to the OpenAI `embeddings` API endpoint.
514
580
  For additional details, see: [https://platform.openai.com/docs/guides/embeddings](https://platform.openai.com/docs/guides/embeddings)
515
581
 
582
+ Request throttling:
583
+ Uses the rate limit-related headers returned by the API to throttle requests adaptively, based on available
584
+ request and token capacity. No configuration is necessary.
585
+
516
586
  __Requirements:__
517
587
 
518
588
  - `pip install openai`
@@ -532,14 +602,24 @@ async def embeddings(
532
602
  Add a computed column that applies the model `text-embedding-3-small` to an existing
533
603
  Pixeltable column `tbl.text` of the table `tbl`:
534
604
 
535
- >>> tbl['embed'] = embeddings(tbl.text, model='text-embedding-3-small')
605
+ >>> tbl.add_computed_column(embed=embeddings(tbl.text, model='text-embedding-3-small'))
606
+
607
+ Add an embedding index to an existing column `text`, using the model `text-embedding-3-small`:
608
+
609
+ >>> tbl.add_embedding_index(embedding=embeddings.using(model='text-embedding-3-small'))
536
610
  """
537
611
  _logger.debug(f'embeddings: batch_size={len(input)}')
538
- resource_pool = _resource_pool(model)
612
+ resource_pool = _rate_limits_pool(model)
539
613
  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'
614
+ resource_pool, lambda: OpenAIRateLimitsInfo(_embeddings_get_request_resources)
615
+ )
616
+ result = await _openai_client().embeddings.with_raw_response.create(
617
+ input=input,
618
+ model=model,
619
+ dimensions=_opt(dimensions),
620
+ user=_opt(user),
621
+ encoding_format='float',
622
+ timeout=_opt(timeout),
543
623
  )
544
624
  requests_info, tokens_info = _get_header_info(result.headers)
545
625
  rate_limits_info.record(requests=requests_info, tokens=tokens_info)
@@ -561,7 +641,7 @@ def _(model: str, dimensions: Optional[int] = None) -> pxt.ArrayType:
561
641
 
562
642
 
563
643
  @pxt.udf
564
- def image_generations(
644
+ async def image_generations(
565
645
  prompt: str,
566
646
  *,
567
647
  model: str = 'dall-e-2',
@@ -569,6 +649,7 @@ def image_generations(
569
649
  size: Optional[str] = None,
570
650
  style: Optional[str] = None,
571
651
  user: Optional[str] = None,
652
+ timeout: Optional[float] = None,
572
653
  ) -> PIL.Image.Image:
573
654
  """
574
655
  Creates an image given a prompt.
@@ -576,6 +657,10 @@ def image_generations(
576
657
  Equivalent to the OpenAI `images/generations` API endpoint.
577
658
  For additional details, see: [https://platform.openai.com/docs/guides/images](https://platform.openai.com/docs/guides/images)
578
659
 
660
+ Request throttling:
661
+ Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
662
+ limit is configured, uses a default of 600 RPM.
663
+
579
664
  __Requirements:__
580
665
 
581
666
  - `pip install openai`
@@ -593,17 +678,18 @@ def image_generations(
593
678
  Add a computed column that applies the model `dall-e-2` to an existing
594
679
  Pixeltable column `tbl.text` of the table `tbl`:
595
680
 
596
- >>> tbl['gen_image'] = image_generations(tbl.text, model='dall-e-2')
681
+ >>> tbl.add_computed_column(gen_image=image_generations(tbl.text, model='dall-e-2'))
597
682
  """
598
683
  # TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
599
- result = _retry(_openai_client().images.generate)(
684
+ result = await _openai_client().images.generate(
600
685
  prompt=prompt,
601
686
  model=_opt(model),
602
- quality=_opt(quality),
603
- size=_opt(size),
604
- style=_opt(style),
687
+ quality=_opt(quality), # type: ignore
688
+ size=_opt(size), # type: ignore
689
+ style=_opt(style), # type: ignore
605
690
  user=_opt(user),
606
691
  response_format='b64_json',
692
+ timeout=_opt(timeout),
607
693
  )
608
694
  b64_str = result.data[0].b64_json
609
695
  b64_bytes = base64.b64decode(b64_str)
@@ -620,7 +706,7 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
620
706
  if x_pos == -1:
621
707
  return pxt.ImageType()
622
708
  try:
623
- width, height = int(size[:x_pos]), int(size[x_pos + 1:])
709
+ width, height = int(size[:x_pos]), int(size[x_pos + 1 :])
624
710
  except ValueError:
625
711
  return pxt.ImageType()
626
712
  return pxt.ImageType(size=(width, height))
@@ -631,13 +717,17 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
631
717
 
632
718
 
633
719
  @pxt.udf
634
- def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
720
+ async def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
635
721
  """
636
722
  Classifies if text is potentially harmful.
637
723
 
638
724
  Equivalent to the OpenAI `moderations` API endpoint.
639
725
  For additional details, see: [https://platform.openai.com/docs/guides/moderation](https://platform.openai.com/docs/guides/moderation)
640
726
 
727
+ Request throttling:
728
+ Applies the rate limit set in the config (section `openai.rate_limits`; use the model id as the key). If no rate
729
+ limit is configured, uses a default of 600 RPM.
730
+
641
731
  __Requirements:__
642
732
 
643
733
  - `pip install openai`
@@ -655,22 +745,26 @@ def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
655
745
  Add a computed column that applies the model `text-moderation-stable` to an existing
656
746
  Pixeltable column `tbl.input` of the table `tbl`:
657
747
 
658
- >>> tbl['moderations'] = moderations(tbl.text, model='text-moderation-stable')
748
+ >>> tbl.add_computed_column(moderations=moderations(tbl.text, model='text-moderation-stable'))
659
749
  """
660
- result = _retry(_openai_client().moderations.create)(input=input, model=_opt(model))
750
+ result = await _openai_client().moderations.create(input=input, model=_opt(model))
661
751
  return result.dict()
662
752
 
663
753
 
664
- # @speech.resource_pool
665
- # @transcriptions.resource_pool
666
- # @translations.resource_pool
754
+ @speech.resource_pool
755
+ @transcriptions.resource_pool
756
+ @translations.resource_pool
757
+ @image_generations.resource_pool
758
+ @moderations.resource_pool
759
+ def _(model: str) -> str:
760
+ return f'request-rate:openai:{model}'
761
+
762
+
667
763
  @chat_completions.resource_pool
668
- # @vision.resource_pool
764
+ @vision.resource_pool
669
765
  @embeddings.resource_pool
670
- # @image_generations.resource_pool
671
- # @moderations.resource_pool
672
766
  def _(model: str) -> str:
673
- return _resource_pool(model)
767
+ return _rate_limits_pool(model)
674
768
 
675
769
 
676
770
  def invoke_tools(tools: Tools, response: exprs.Expr) -> exprs.InlineDict:
@@ -684,9 +778,7 @@ def _openai_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
684
778
  return None
685
779
  openai_tool_calls = response['choices'][0]['message']['tool_calls']
686
780
  return {
687
- tool_call['function']['name']: {
688
- 'args': json.loads(tool_call['function']['arguments'])
689
- }
781
+ tool_call['function']['name']: {'args': json.loads(tool_call['function']['arguments'])}
690
782
  for tool_call in openai_tool_calls
691
783
  }
692
784
 
@@ -696,6 +788,7 @@ _T = TypeVar('_T')
696
788
 
697
789
  def _opt(arg: _T) -> Union[_T, 'openai.NotGiven']:
698
790
  import openai
791
+
699
792
  return arg if arg is not None else openai.NOT_GIVEN
700
793
 
701
794