pixeltable 0.3.1__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 +25 -15
  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 +123 -103
  14. pixeltable/catalog/table_version.py +292 -143
  15. pixeltable/catalog/table_version_path.py +8 -5
  16. pixeltable/catalog/view.py +68 -27
  17. pixeltable/dataframe.py +102 -72
  18. pixeltable/env.py +39 -23
  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 +18 -17
  27. pixeltable/exec/expr_eval/expr_eval_node.py +29 -16
  28. pixeltable/exec/expr_eval/globals.py +33 -11
  29. pixeltable/exec/expr_eval/row_buffer.py +5 -6
  30. pixeltable/exec/expr_eval/schedulers.py +170 -42
  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 +31 -16
  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 +21 -15
  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 +214 -109
  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 +61 -28
  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 +3 -2
  101. pixeltable/io/label_studio.py +80 -71
  102. pixeltable/io/pandas.py +33 -9
  103. pixeltable/io/parquet.py +10 -13
  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 +9 -2
  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 +130 -85
  129. pixeltable/utils/arrow.py +1 -7
  130. pixeltable/utils/coco.py +16 -17
  131. pixeltable/utils/code.py +1 -1
  132. pixeltable/utils/console_output.py +44 -0
  133. pixeltable/utils/description_helper.py +7 -7
  134. pixeltable/utils/documents.py +3 -1
  135. pixeltable/utils/filecache.py +13 -8
  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.1.dist-info → pixeltable-0.3.3.dist-info}/METADATA +7 -8
  143. pixeltable-0.3.3.dist-info/RECORD +163 -0
  144. pixeltable-0.3.1.dist-info/RECORD +0 -160
  145. {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/LICENSE +0 -0
  146. {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/WHEEL +0 -0
  147. {pixeltable-0.3.1.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,8 +94,18 @@ 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
- openai.RateLimitError, openai.APITimeoutError, openai.UnprocessableEntityError, openai.InternalServerError
99
+ # ConnectionError: we occasionally see this error when the AsyncConnectionPool is trying to close
100
+ # expired connections
101
+ # (AsyncConnectionPool._close_expired_connections() fails with ConnectionError when executing
102
+ # 'await connection.aclose()', which is very likely a bug in AsyncConnectionPool)
103
+ openai.APIConnectionError,
104
+ # the following errors are retryable according to OpenAI's API documentation
105
+ openai.RateLimitError,
106
+ openai.APITimeoutError,
107
+ openai.UnprocessableEntityError,
108
+ openai.InternalServerError,
117
109
  )
118
110
 
119
111
  def get_retry_delay(self, exc: Exception) -> Optional[float]:
@@ -133,7 +125,7 @@ _header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d
133
125
  def _parse_header_duration(duration_str):
134
126
  match = _header_duration_pattern.match(duration_str)
135
127
  if not match:
136
- raise ValueError("Invalid duration format")
128
+ raise ValueError('Invalid duration format')
137
129
 
138
130
  days = int(match.group(1) or 0)
139
131
  hours = int(match.group(2) or 0)
@@ -141,17 +133,11 @@ def _parse_header_duration(duration_str):
141
133
  minutes = int(match.group(4) or 0)
142
134
  seconds = float(match.group(5) or 0)
143
135
 
144
- return datetime.timedelta(
145
- days=days,
146
- hours=hours,
147
- minutes=minutes,
148
- seconds=seconds,
149
- milliseconds=milliseconds
150
- )
136
+ return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
151
137
 
152
138
 
153
139
  def _get_header_info(
154
- headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
140
+ headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
155
141
  ) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
156
142
  assert requests or tokens
157
143
  now = datetime.datetime.now(tz=datetime.timezone.utc)
@@ -184,8 +170,14 @@ def _get_header_info(
184
170
 
185
171
 
186
172
  @pxt.udf
187
- def speech(
188
- 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,
189
181
  ) -> pxt.Audio:
190
182
  """
191
183
  Generates audio from the input text.
@@ -193,6 +185,10 @@ def speech(
193
185
  Equivalent to the OpenAI `audio/speech` API endpoint.
194
186
  For additional details, see: [https://platform.openai.com/docs/guides/text-to-speech](https://platform.openai.com/docs/guides/text-to-speech)
195
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
+
196
192
  __Requirements:__
197
193
 
198
194
  - `pip install openai`
@@ -212,10 +208,15 @@ def speech(
212
208
  Add a computed column that applies the model `tts-1` to an existing Pixeltable column `tbl.text`
213
209
  of the table `tbl`:
214
210
 
215
- >>> tbl['audio'] = speech(tbl.text, model='tts-1', voice='nova')
211
+ >>> tbl.add_computed_column(audio=speech(tbl.text, model='tts-1', voice='nova'))
216
212
  """
217
- content = _retry(_openai_client().audio.speech.create)(
218
- 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),
219
220
  )
220
221
  ext = response_format or 'mp3'
221
222
  output_filename = str(env.Env.get().tmp_dir / f'{uuid.uuid4()}.{ext}')
@@ -224,13 +225,14 @@ def speech(
224
225
 
225
226
 
226
227
  @pxt.udf
227
- def transcriptions(
228
+ async def transcriptions(
228
229
  audio: pxt.Audio,
229
230
  *,
230
231
  model: str,
231
232
  language: Optional[str] = None,
232
233
  prompt: Optional[str] = None,
233
234
  temperature: Optional[float] = None,
235
+ timeout: Optional[float] = None,
234
236
  ) -> dict:
235
237
  """
236
238
  Transcribes audio into the input language.
@@ -238,6 +240,10 @@ def transcriptions(
238
240
  Equivalent to the OpenAI `audio/transcriptions` API endpoint.
239
241
  For additional details, see: [https://platform.openai.com/docs/guides/speech-to-text](https://platform.openai.com/docs/guides/speech-to-text)
240
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
+
241
247
  __Requirements:__
242
248
 
243
249
  - `pip install openai`
@@ -255,22 +261,28 @@ def transcriptions(
255
261
  Add a computed column that applies the model `whisper-1` to an existing Pixeltable column `tbl.audio`
256
262
  of the table `tbl`:
257
263
 
258
- >>> tbl['transcription'] = transcriptions(tbl.audio, model='whisper-1', language='en')
264
+ >>> tbl.add_computed_column(transcription=transcriptions(tbl.audio, model='whisper-1', language='en'))
259
265
  """
260
266
  file = pathlib.Path(audio)
261
- transcription = _retry(_openai_client().audio.transcriptions.create)(
262
- 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),
263
274
  )
264
275
  return transcription.dict()
265
276
 
266
277
 
267
278
  @pxt.udf
268
- def translations(
279
+ async def translations(
269
280
  audio: pxt.Audio,
270
281
  *,
271
282
  model: str,
272
283
  prompt: Optional[str] = None,
273
- temperature: Optional[float] = None
284
+ temperature: Optional[float] = None,
285
+ timeout: Optional[float] = None,
274
286
  ) -> dict:
275
287
  """
276
288
  Translates audio into English.
@@ -278,6 +290,10 @@ def translations(
278
290
  Equivalent to the OpenAI `audio/translations` API endpoint.
279
291
  For additional details, see: [https://platform.openai.com/docs/guides/speech-to-text](https://platform.openai.com/docs/guides/speech-to-text)
280
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
+
281
297
  __Requirements:__
282
298
 
283
299
  - `pip install openai`
@@ -295,11 +311,11 @@ def translations(
295
311
  Add a computed column that applies the model `whisper-1` to an existing Pixeltable column `tbl.audio`
296
312
  of the table `tbl`:
297
313
 
298
- >>> tbl['translation'] = translations(tbl.audio, model='whisper-1', language='en')
314
+ >>> tbl.add_computed_column(translation=translations(tbl.audio, model='whisper-1', language='en'))
299
315
  """
300
316
  file = pathlib.Path(audio)
301
- translation = _retry(_openai_client().audio.translations.create)(
302
- 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)
303
319
  )
304
320
  return translation.dict()
305
321
 
@@ -309,7 +325,7 @@ def translations(
309
325
 
310
326
 
311
327
  def _chat_completions_get_request_resources(
312
- messages: list, max_tokens: Optional[int], n: Optional[int]
328
+ messages: list, max_tokens: Optional[int], n: Optional[int]
313
329
  ) -> dict[str, int]:
314
330
  completion_tokens = n * max_tokens
315
331
 
@@ -318,7 +334,7 @@ def _chat_completions_get_request_resources(
318
334
  num_tokens += 4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
319
335
  for key, value in message.items():
320
336
  num_tokens += len(value) / 4
321
- 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
322
338
  num_tokens -= 1 # role is always required and always 1 token
323
339
  num_tokens += 2 # every reply is primed with <im_start>assistant
324
340
  return {'requests': 1, 'tokens': int(num_tokens) + completion_tokens}
@@ -344,6 +360,7 @@ async def chat_completions(
344
360
  tools: Optional[list[dict]] = None,
345
361
  tool_choice: Optional[dict] = None,
346
362
  user: Optional[str] = None,
363
+ timeout: Optional[float] = None,
347
364
  ) -> dict:
348
365
  """
349
366
  Creates a model response for the given chat conversation.
@@ -351,6 +368,10 @@ async def chat_completions(
351
368
  Equivalent to the OpenAI `chat/completions` API endpoint.
352
369
  For additional details, see: [https://platform.openai.com/docs/guides/chat-completions](https://platform.openai.com/docs/guides/chat-completions)
353
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
+
354
375
  __Requirements:__
355
376
 
356
377
  - `pip install openai`
@@ -372,16 +393,10 @@ async def chat_completions(
372
393
  {'role': 'system', 'content': 'You are a helpful assistant.'},
373
394
  {'role': 'user', 'content': tbl.prompt}
374
395
  ]
375
- tbl['response'] = chat_completions(messages, model='gpt-4o-mini')
396
+ tbl.add_computed_column(response=chat_completions(messages, model='gpt-4o-mini'))
376
397
  """
377
398
  if tools is not None:
378
- tools = [
379
- {
380
- 'type': 'function',
381
- 'function': tool
382
- }
383
- for tool in tools
384
- ]
399
+ tools = [{'type': 'function', 'function': tool} for tool in tools]
385
400
 
386
401
  tool_choice_: Union[str, dict, None] = None
387
402
  if tool_choice is not None:
@@ -391,17 +406,20 @@ async def chat_completions(
391
406
  tool_choice_ = 'required'
392
407
  else:
393
408
  assert tool_choice['tool'] is not None
394
- tool_choice_ = {
395
- 'type': 'function',
396
- 'function': {'name': tool_choice['tool']}
397
- }
409
+ tool_choice_ = {'type': 'function', 'function': {'name': tool_choice['tool']}}
398
410
 
399
411
  extra_body: Optional[dict[str, Any]] = None
400
412
  if tool_choice is not None and not tool_choice['parallel_tool_calls']:
401
413
  extra_body = {'parallel_tool_calls': False}
402
414
 
415
+ # make sure the pool info exists prior to making the request
416
+ resource_pool = _rate_limits_pool(model)
417
+ rate_limits_info = env.Env.get().get_resource_pool_info(
418
+ resource_pool, lambda: OpenAIRateLimitsInfo(_chat_completions_get_request_resources)
419
+ )
420
+
403
421
  # cast(Any, ...): avoid mypy errors
404
- result = await _async_openai_client().chat.completions.with_raw_response.create(
422
+ result = await _openai_client().chat.completions.with_raw_response.create(
405
423
  messages=messages,
406
424
  model=model,
407
425
  frequency_penalty=_opt(frequency_penalty),
@@ -419,27 +437,65 @@ async def chat_completions(
419
437
  tools=_opt(cast(Any, tools)),
420
438
  tool_choice=_opt(cast(Any, tool_choice_)),
421
439
  user=_opt(user),
422
- timeout=10,
440
+ timeout=_opt(timeout),
423
441
  extra_body=extra_body,
424
442
  )
425
443
 
426
- resource_pool = _resource_pool(model)
427
444
  requests_info, tokens_info = _get_header_info(result.headers)
428
- rate_limits_info = env.Env.get().get_resource_pool_info(resource_pool, lambda: OpenAIRateLimitsInfo(
429
- _chat_completions_get_request_resources))
430
445
  rate_limits_info.record(requests=requests_info, tokens=tokens_info)
431
446
 
432
447
  return json.loads(result.text)
433
448
 
434
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
+
435
479
  @pxt.udf
436
- 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:
437
489
  """
438
490
  Analyzes an image with the OpenAI vision capability. This is a convenience function that takes an image and
439
491
  prompt, and constructs a chat completion request that utilizes OpenAI vision.
440
492
 
441
493
  For additional details, see: [https://platform.openai.com/docs/guides/vision](https://platform.openai.com/docs/guides/vision)
442
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
+
443
499
  __Requirements:__
444
500
 
445
501
  - `pip install openai`
@@ -456,7 +512,7 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
456
512
  Add a computed column that applies the model `gpt-4o-mini` to an existing Pixeltable column `tbl.image`
457
513
  of the table `tbl`:
458
514
 
459
- >>> 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'))
460
516
  """
461
517
  # TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
462
518
  bytes_arr = io.BytesIO()
@@ -472,8 +528,25 @@ def vision(prompt: str, image: PIL.Image.Image, *, model: str) -> str:
472
528
  ],
473
529
  }
474
530
  ]
475
- result = _retry(_openai_client().chat.completions.create)(messages=messages, model=model)
476
- 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']
477
550
 
478
551
 
479
552
  #####################################
@@ -493,7 +566,12 @@ def _embeddings_get_request_resources(input: list[str]) -> dict[str, int]:
493
566
 
494
567
  @pxt.udf(batch_size=32)
495
568
  async def embeddings(
496
- 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,
497
575
  ) -> Batch[pxt.Array[(None,), pxt.Float]]:
498
576
  """
499
577
  Creates an embedding vector representing the input text.
@@ -501,6 +579,10 @@ async def embeddings(
501
579
  Equivalent to the OpenAI `embeddings` API endpoint.
502
580
  For additional details, see: [https://platform.openai.com/docs/guides/embeddings](https://platform.openai.com/docs/guides/embeddings)
503
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
+
504
586
  __Requirements:__
505
587
 
506
588
  - `pip install openai`
@@ -520,16 +602,26 @@ async def embeddings(
520
602
  Add a computed column that applies the model `text-embedding-3-small` to an existing
521
603
  Pixeltable column `tbl.text` of the table `tbl`:
522
604
 
523
- >>> 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'))
524
610
  """
525
611
  _logger.debug(f'embeddings: batch_size={len(input)}')
526
- result = await _async_openai_client().embeddings.with_raw_response.create(
527
- input=input, model=model, dimensions=_opt(dimensions), user=_opt(user), encoding_format='float'
612
+ resource_pool = _rate_limits_pool(model)
613
+ rate_limits_info = env.Env.get().get_resource_pool_info(
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),
528
623
  )
529
- resource_pool = _resource_pool(model)
530
624
  requests_info, tokens_info = _get_header_info(result.headers)
531
- rate_limits_info = env.Env.get().get_resource_pool_info(
532
- resource_pool, lambda: OpenAIRateLimitsInfo(_embeddings_get_request_resources))
533
625
  rate_limits_info.record(requests=requests_info, tokens=tokens_info)
534
626
  return [np.array(data['embedding'], dtype=np.float64) for data in json.loads(result.content)['data']]
535
627
 
@@ -549,7 +641,7 @@ def _(model: str, dimensions: Optional[int] = None) -> pxt.ArrayType:
549
641
 
550
642
 
551
643
  @pxt.udf
552
- def image_generations(
644
+ async def image_generations(
553
645
  prompt: str,
554
646
  *,
555
647
  model: str = 'dall-e-2',
@@ -557,6 +649,7 @@ def image_generations(
557
649
  size: Optional[str] = None,
558
650
  style: Optional[str] = None,
559
651
  user: Optional[str] = None,
652
+ timeout: Optional[float] = None,
560
653
  ) -> PIL.Image.Image:
561
654
  """
562
655
  Creates an image given a prompt.
@@ -564,6 +657,10 @@ def image_generations(
564
657
  Equivalent to the OpenAI `images/generations` API endpoint.
565
658
  For additional details, see: [https://platform.openai.com/docs/guides/images](https://platform.openai.com/docs/guides/images)
566
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
+
567
664
  __Requirements:__
568
665
 
569
666
  - `pip install openai`
@@ -581,17 +678,18 @@ def image_generations(
581
678
  Add a computed column that applies the model `dall-e-2` to an existing
582
679
  Pixeltable column `tbl.text` of the table `tbl`:
583
680
 
584
- >>> 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'))
585
682
  """
586
683
  # TODO(aaron-siegel): Decompose CPU/GPU ops into separate functions
587
- result = _retry(_openai_client().images.generate)(
684
+ result = await _openai_client().images.generate(
588
685
  prompt=prompt,
589
686
  model=_opt(model),
590
- quality=_opt(quality),
591
- size=_opt(size),
592
- style=_opt(style),
687
+ quality=_opt(quality), # type: ignore
688
+ size=_opt(size), # type: ignore
689
+ style=_opt(style), # type: ignore
593
690
  user=_opt(user),
594
691
  response_format='b64_json',
692
+ timeout=_opt(timeout),
595
693
  )
596
694
  b64_str = result.data[0].b64_json
597
695
  b64_bytes = base64.b64decode(b64_str)
@@ -608,7 +706,7 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
608
706
  if x_pos == -1:
609
707
  return pxt.ImageType()
610
708
  try:
611
- width, height = int(size[:x_pos]), int(size[x_pos + 1:])
709
+ width, height = int(size[:x_pos]), int(size[x_pos + 1 :])
612
710
  except ValueError:
613
711
  return pxt.ImageType()
614
712
  return pxt.ImageType(size=(width, height))
@@ -619,13 +717,17 @@ def _(size: Optional[str] = None) -> pxt.ImageType:
619
717
 
620
718
 
621
719
  @pxt.udf
622
- def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
720
+ async def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
623
721
  """
624
722
  Classifies if text is potentially harmful.
625
723
 
626
724
  Equivalent to the OpenAI `moderations` API endpoint.
627
725
  For additional details, see: [https://platform.openai.com/docs/guides/moderation](https://platform.openai.com/docs/guides/moderation)
628
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
+
629
731
  __Requirements:__
630
732
 
631
733
  - `pip install openai`
@@ -643,22 +745,26 @@ def moderations(input: str, *, model: str = 'omni-moderation-latest') -> dict:
643
745
  Add a computed column that applies the model `text-moderation-stable` to an existing
644
746
  Pixeltable column `tbl.input` of the table `tbl`:
645
747
 
646
- >>> tbl['moderations'] = moderations(tbl.text, model='text-moderation-stable')
748
+ >>> tbl.add_computed_column(moderations=moderations(tbl.text, model='text-moderation-stable'))
647
749
  """
648
- result = _retry(_openai_client().moderations.create)(input=input, model=_opt(model))
750
+ result = await _openai_client().moderations.create(input=input, model=_opt(model))
649
751
  return result.dict()
650
752
 
651
753
 
652
- # @speech.resource_pool
653
- # @transcriptions.resource_pool
654
- # @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
+
655
763
  @chat_completions.resource_pool
656
- # @vision.resource_pool
764
+ @vision.resource_pool
657
765
  @embeddings.resource_pool
658
- # @image_generations.resource_pool
659
- # @moderations.resource_pool
660
766
  def _(model: str) -> str:
661
- return _resource_pool(model)
767
+ return _rate_limits_pool(model)
662
768
 
663
769
 
664
770
  def invoke_tools(tools: Tools, response: exprs.Expr) -> exprs.InlineDict:
@@ -672,9 +778,7 @@ def _openai_response_to_pxt_tool_calls(response: dict) -> Optional[dict]:
672
778
  return None
673
779
  openai_tool_calls = response['choices'][0]['message']['tool_calls']
674
780
  return {
675
- tool_call['function']['name']: {
676
- 'args': json.loads(tool_call['function']['arguments'])
677
- }
781
+ tool_call['function']['name']: {'args': json.loads(tool_call['function']['arguments'])}
678
782
  for tool_call in openai_tool_calls
679
783
  }
680
784
 
@@ -684,6 +788,7 @@ _T = TypeVar('_T')
684
788
 
685
789
  def _opt(arg: _T) -> Union[_T, 'openai.NotGiven']:
686
790
  import openai
791
+
687
792
  return arg if arg is not None else openai.NOT_GIVEN
688
793
 
689
794