From 60aed096148bdc1527a1d1c0e0f3ca61842f28f4 Mon Sep 17 00:00:00 2001
From: markstuart-oai <323302876+markstuart-oai@users.noreply.github.com>
Date: Mon, 21 Sep 2026 06:08:35 +0000
Subject: [PATCH] fix(api): preserve audio formats and diarized transcription
types
Castiron-Internal-PR: https://github.com/openai/openai-python-internal/pull/121
Castiron-Source-SHA: f1cabf8850274ebf78d553df3bc1c5b11b7bebb4
Castiron-Public-Base-SHA: 69a2c1db6feacf32be6693809e7cab1c3b49cad7
---
.castiron.stats.yml | 14 +-
api.md | 54 ++---
api_reference/openapi.transformed.yml | 206 +++++++++++++++---
scripts/utils/validate-httpx2-wheel.py | 36 ++-
.../resources/admin/organization/__init__.py | 24 +-
.../admin/organization/organization.py | 48 ++--
src/openai/resources/audio/transcriptions.py | 63 +++++-
src/openai/resources/safety/__init__.py | 24 +-
src/openai/resources/safety/safety.py | 48 ++--
tests/lib/test_audio_diarized_types.py | 64 ++++++
.../lib/test_audio_transcription_multipart.py | 148 +++++++++++++
11 files changed, 576 insertions(+), 153 deletions(-)
create mode 100644 tests/lib/test_audio_diarized_types.py
create mode 100644 tests/lib/test_audio_transcription_multipart.py
diff --git a/.castiron.stats.yml b/.castiron.stats.yml
index 944ef561cc..a65f41b2fd 100644
--- a/.castiron.stats.yml
+++ b/.castiron.stats.yml
@@ -1,8 +1,8 @@
schema_version: 1
-generation_id: feaf88e6-0c6a-4d57-95a5-0bcc23d7105c
-openapi_spec_hash: ce912f79d7f8cb19a7a515513c214b8a
-openapi_transformed_spec_hash: 352b5a7a028f990b6475b6593e381eee
-config_hash: dde98d9c41cd6f4163320f92c70f83e6
-codegen_sha: 799a1f4d5f6b415088641ca4d3bbb804443ec30e
-codegen_hash: 7d4da50897149f4eab2fa63ac3dbc972694c6dfeb79a01531da408010bff5dcd
-public_codegen_sha: 4b121f228fc198913ea2a56404f38d8f18ca0e86
+generation_id: 811c58b1-fc79-472f-966f-d709343450e8
+openapi_spec_hash: 8c73dea75f8570b7ea967542dafa0b0f
+openapi_transformed_spec_hash: 19d647b4e088bfa2499083e65eacd54e
+config_hash: ad0fbf2fbcf7981fe7a9910664285991
+codegen_sha: 252064cd9cef4d4db1f9101d5929573ae9964d7c
+codegen_hash: 30b56c8802b480afdea6feade2b7ea1ec31e9ebce33e9b64f0ff4da1154e2a1b
+public_codegen_sha: 660cdc509c69dafb285c7c5cba7fd1781276d33c
diff --git a/api.md b/api.md
index dd48a632c8..fb2f91789f 100644
--- a/api.md
+++ b/api.md
@@ -451,29 +451,29 @@ Methods:
# Safety
-## Alerts
+## Cases
Types:
```python
-from openai.types.safety import SafetyAlert
+from openai.types.safety import SafetyCase
```
Methods:
-- client.safety.alerts.retrieve(id) -> SafetyAlert
+- client.safety.cases.retrieve(id) -> SafetyCase
-## Cases
+## Alerts
Types:
```python
-from openai.types.safety import SafetyCase
+from openai.types.safety import SafetyAlert
```
Methods:
-- client.safety.cases.retrieve(id) -> SafetyCase
+- client.safety.alerts.retrieve(id) -> SafetyAlert
# [Webhooks](src/openai/resources/webhooks/api.md)
@@ -1299,27 +1299,6 @@ Methods:
## Organization
-### ExternalStorage
-
-Types:
-
-```python
-from openai.types.admin.organization import (
- AwsExternalStorageProvider,
- AzureExternalStorageProvider,
- ExternalStorageConfiguration,
- ExternalStorageDeleted,
-)
-```
-
-Methods:
-
-- client.admin.organization.external_storage.create(\*\*params) -> ExternalStorageConfiguration
-- client.admin.organization.external_storage.retrieve(external_storage_id) -> ExternalStorageConfiguration
-- client.admin.organization.external_storage.list(\*\*params) -> SyncCursorPage[ExternalStorageConfiguration]
-- client.admin.organization.external_storage.delete(external_storage_id) -> ExternalStorageDeleted
-- client.admin.organization.external_storage.validate(external_storage_id) -> ExternalStorageConfiguration
-
### AuditLogs
Types:
@@ -1521,6 +1500,27 @@ Methods:
- client.admin.organization.data_retention.retrieve() -> OrganizationDataRetention
- client.admin.organization.data_retention.update(\*\*params) -> OrganizationDataRetention
+### ExternalStorage
+
+Types:
+
+```python
+from openai.types.admin.organization import (
+ AwsExternalStorageProvider,
+ AzureExternalStorageProvider,
+ ExternalStorageConfiguration,
+ ExternalStorageDeleted,
+)
+```
+
+Methods:
+
+- client.admin.organization.external_storage.create(\*\*params) -> ExternalStorageConfiguration
+- client.admin.organization.external_storage.retrieve(external_storage_id) -> ExternalStorageConfiguration
+- client.admin.organization.external_storage.list(\*\*params) -> SyncCursorPage[ExternalStorageConfiguration]
+- client.admin.organization.external_storage.delete(external_storage_id) -> ExternalStorageDeleted
+- client.admin.organization.external_storage.validate(external_storage_id) -> ExternalStorageConfiguration
+
### SpendLimit
Types:
diff --git a/api_reference/openapi.transformed.yml b/api_reference/openapi.transformed.yml
index 30f3e65810..f8d2c45991 100644
--- a/api_reference/openapi.transformed.yml
+++ b/api_reference/openapi.transformed.yml
@@ -601,11 +601,64 @@ paths:
schema:
type: string
format: binary
+ audio/mpeg:
+ schema:
+ type: string
+ format: binary
+ audio/aac:
+ schema:
+ type: string
+ format: binary
+ audio/opus:
+ schema:
+ type: string
+ format: binary
+ audio/flac:
+ schema:
+ type: string
+ format: binary
+ audio/pcm:
+ schema:
+ type: string
+ format: binary
+ audio/wav:
+ schema:
+ type: string
+ format: binary
text/event-stream:
schema:
$ref: '#/components/schemas/CreateSpeechResponseStreamEvent'
+ '400':
+ description: Invalid speech request, input, output format, or voice.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Authentication failed because the API key is missing or revoked, or the client IP is not authorized.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ text/plain:
+ schema:
+ type: string
+ description: JSON-encoded error text containing an error object and the HTTP status.
+ '403':
+ description: Access to a personal API organization is blocked by the organization policy.
+ content:
+ text/plain:
+ schema:
+ type: string
+ description: JSON-encoded error text containing an error object and the HTTP status.
'429':
$ref: '#/components/responses/InferenceRateLimited'
+ '500':
+ description: The voice or speech audio could not be processed.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'503':
$ref: '#/components/responses/InferenceServiceUnavailable'
x-oaiMeta:
@@ -694,7 +747,8 @@ paths:
Transcribes audio into the input language.
Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
- format, or a stream of transcript events.
+ format, plain text in `text`, `srt`, or `vtt` format, or a stream of transcript
+ events. Supported formats depend on the model.
requestBody:
required: true
content:
@@ -710,16 +764,58 @@ paths:
anyOf:
- $ref: '#/components/schemas/CreateTranscriptionResponseJson'
- $ref: '#/components/schemas/CreateTranscriptionResponseDiarizedJson'
- x-stainless-skip:
- - go
- $ref: '#/components/schemas/CreateTranscriptionResponseVerboseJson'
discriminator:
propertyName: task
+ text/plain:
+ schema:
+ type: string
text/event-stream:
schema:
$ref: '#/components/schemas/CreateTranscriptionResponseStreamEvent'
+ '400':
+ description: Invalid audio input or request parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Authentication failed because the API key is missing or revoked, or the client IP is not authorized.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ text/plain:
+ schema:
+ type: string
+ description: JSON-encoded error text containing an error object and the HTTP status.
+ '403':
+ description: Access to a personal API organization is blocked by the organization policy.
+ content:
+ text/plain:
+ schema:
+ type: string
+ description: JSON-encoded error text containing an error object and the HTTP status.
+ '413':
+ description: The audio request exceeds the supported size limit.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'429':
$ref: '#/components/responses/InferenceRateLimited'
+ '500':
+ description: The audio could not be processed.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '502':
+ description: The upstream audio service connection failed.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'503':
$ref: '#/components/responses/InferenceServiceUnavailable'
x-oaiMeta:
@@ -1271,8 +1367,52 @@ paths:
- $ref: '#/components/schemas/CreateTranslationResponseVerboseJson'
x-stainless-skip:
- go
+ text/plain:
+ schema:
+ type: string
+ '400':
+ description: Invalid audio input or request parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Authentication failed because the API key is missing or revoked, or the client IP is not authorized.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ text/plain:
+ schema:
+ type: string
+ description: JSON-encoded error text containing an error object and the HTTP status.
+ '403':
+ description: Access to a personal API organization is blocked by the organization policy.
+ content:
+ text/plain:
+ schema:
+ type: string
+ description: JSON-encoded error text containing an error object and the HTTP status.
+ '413':
+ description: The audio request exceeds the supported size limit.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'429':
$ref: '#/components/responses/InferenceRateLimited'
+ '500':
+ description: The audio could not be processed.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '502':
+ description: The upstream audio service connection failed.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'503':
$ref: '#/components/responses/InferenceServiceUnavailable'
x-oaiMeta:
@@ -66730,6 +66870,36 @@ components:
"type": "safety.warning_issued",
"data": {"id": "C-abc123"}
}
+ _MisalignmentErrorType:
+ anyOf:
+ - type: string
+ - type: string
+ enum:
+ - potentially_unintended_data_transfer
+ - potentially_unintended_data_access
+ - potentially_unintended_destructive_activity
+ - other
+ _MisalignmentSteer:
+ properties:
+ message:
+ type: string
+ description: The public continuation instruction.
+ type: object
+ required:
+ - message
+ MisalignmentErrorDetailsResource:
+ properties:
+ error_type:
+ $ref: '#/components/schemas/_MisalignmentErrorType'
+ description: An optional classification; clients must accept additional values.
+ detailed_explanation:
+ type: string
+ description: The public explanation for this block.
+ steer:
+ $ref: '#/components/schemas/_MisalignmentSteer'
+ description: An optional public continuation instruction.
+ type: object
+ required: []
ModerationInputType:
type: string
enum:
@@ -70206,36 +70376,6 @@ components:
- object
- metadata
- created_at
- _MisalignmentErrorType:
- anyOf:
- - type: string
- - type: string
- enum:
- - potentially_unintended_data_transfer
- - potentially_unintended_data_access
- - potentially_unintended_destructive_activity
- - other
- _MisalignmentSteer:
- properties:
- message:
- type: string
- description: The public continuation instruction.
- type: object
- required:
- - message
- MisalignmentErrorDetailsResource:
- properties:
- error_type:
- $ref: '#/components/schemas/_MisalignmentErrorType'
- description: An optional classification; clients must accept additional values.
- detailed_explanation:
- type: string
- description: The public explanation for this block.
- steer:
- $ref: '#/components/schemas/_MisalignmentSteer'
- description: An optional public continuation instruction.
- type: object
- required: []
ImageGenOutputTokensDetails:
properties:
image_tokens:
diff --git a/scripts/utils/validate-httpx2-wheel.py b/scripts/utils/validate-httpx2-wheel.py
index 2d3e0bba5d..91ef3aa953 100644
--- a/scripts/utils/validate-httpx2-wheel.py
+++ b/scripts/utils/validate-httpx2-wheel.py
@@ -14,6 +14,7 @@
HTTPX2_TEST = ROOT / "tests/test_httpx2.py"
LEGACY_TEST = ROOT / "tests/test_httpx_compat.py"
TLS_TEST = ROOT / "tests/test_tls_hostname.py"
+MULTIPART_TEST = ROOT / "tests/lib/test_audio_transcription_multipart.py"
def venv_python(environment_path: Path) -> Path:
@@ -76,17 +77,42 @@ def run_case(
check=True,
)
+ test_environment = environment.copy()
+ for name in list(test_environment):
+ if name.startswith(("OPENAI_", "AZURE_OPENAI_")) or name in (
+ "ALL_PROXY",
+ "HTTPS_PROXY",
+ "HTTP_PROXY",
+ "all_proxy",
+ "https_proxy",
+ "http_proxy",
+ ):
+ test_environment.pop(name)
+ subprocess.run(
+ [
+ str(python),
+ "-c",
+ "".join(
+ [
+ "from pathlib import Path; import openai, sysconfig; ",
+ "origin = Path(openai.__file__).resolve(); ",
+ "assert origin.is_relative_to(Path(sysconfig.get_path('purelib')).resolve()), origin",
+ ]
+ ),
+ ],
+ cwd=directory,
+ env=test_environment,
+ check=True,
+ )
+
if not legacy:
subprocess.run(
[str(python), "-c", "import importlib.util; assert importlib.util.find_spec('httpx') is None"],
cwd=directory,
- env=environment,
+ env=test_environment,
check=True,
)
- test_environment = environment.copy()
- for name in ("ALL_PROXY", "HTTPS_PROXY", "HTTP_PROXY", "all_proxy", "https_proxy", "http_proxy"):
- test_environment.pop(name, None)
if legacy:
test_environment["OPENAI_TEST_LEGACY_HTTPX"] = "1"
subprocess.run(
@@ -105,7 +131,7 @@ def main() -> None:
validate_metadata(wheel)
common = ["pytest==9.0.3", "pytest-asyncio==1.4.0", "pygments==2.20.0"]
- run_case(wheel, extra=None, tests=[BASE_TEST, HTTPX2_TEST], dependencies=common)
+ run_case(wheel, extra=None, tests=[BASE_TEST, HTTPX2_TEST, MULTIPART_TEST], dependencies=common)
run_case(wheel, extra="aiohttp", tests=[BASE_TEST], dependencies=common)
run_case(
wheel,
diff --git a/src/openai/resources/admin/organization/__init__.py b/src/openai/resources/admin/organization/__init__.py
index e402b55d71..d1036e4846 100644
--- a/src/openai/resources/admin/organization/__init__.py
+++ b/src/openai/resources/admin/organization/__init__.py
@@ -118,12 +118,6 @@
else:
_EXPORTS = {
- "ExternalStorage": (".external_storage", "ExternalStorage"),
- "AsyncExternalStorage": (".external_storage", "AsyncExternalStorage"),
- "ExternalStorageWithRawResponse": (".external_storage", "ExternalStorageWithRawResponse"),
- "AsyncExternalStorageWithRawResponse": (".external_storage", "AsyncExternalStorageWithRawResponse"),
- "ExternalStorageWithStreamingResponse": (".external_storage", "ExternalStorageWithStreamingResponse"),
- "AsyncExternalStorageWithStreamingResponse": (".external_storage", "AsyncExternalStorageWithStreamingResponse"),
"AuditLogs": (".audit_logs", "AuditLogs"),
"AsyncAuditLogs": (".audit_logs", "AsyncAuditLogs"),
"AuditLogsWithRawResponse": (".audit_logs", "AuditLogsWithRawResponse"),
@@ -172,6 +166,12 @@
"AsyncDataRetentionWithRawResponse": (".data_retention", "AsyncDataRetentionWithRawResponse"),
"DataRetentionWithStreamingResponse": (".data_retention", "DataRetentionWithStreamingResponse"),
"AsyncDataRetentionWithStreamingResponse": (".data_retention", "AsyncDataRetentionWithStreamingResponse"),
+ "ExternalStorage": (".external_storage", "ExternalStorage"),
+ "AsyncExternalStorage": (".external_storage", "AsyncExternalStorage"),
+ "ExternalStorageWithRawResponse": (".external_storage", "ExternalStorageWithRawResponse"),
+ "AsyncExternalStorageWithRawResponse": (".external_storage", "AsyncExternalStorageWithRawResponse"),
+ "ExternalStorageWithStreamingResponse": (".external_storage", "ExternalStorageWithStreamingResponse"),
+ "AsyncExternalStorageWithStreamingResponse": (".external_storage", "AsyncExternalStorageWithStreamingResponse"),
"SpendLimit": (".spend_limit", "SpendLimit"),
"AsyncSpendLimit": (".spend_limit", "AsyncSpendLimit"),
"SpendLimitWithRawResponse": (".spend_limit", "SpendLimitWithRawResponse"),
@@ -238,12 +238,6 @@ def __dir__() -> list[str]:
__all__ = [
- "ExternalStorage",
- "AsyncExternalStorage",
- "ExternalStorageWithRawResponse",
- "AsyncExternalStorageWithRawResponse",
- "ExternalStorageWithStreamingResponse",
- "AsyncExternalStorageWithStreamingResponse",
"AuditLogs",
"AsyncAuditLogs",
"AuditLogsWithRawResponse",
@@ -292,6 +286,12 @@ def __dir__() -> list[str]:
"AsyncDataRetentionWithRawResponse",
"DataRetentionWithStreamingResponse",
"AsyncDataRetentionWithStreamingResponse",
+ "ExternalStorage",
+ "AsyncExternalStorage",
+ "ExternalStorageWithRawResponse",
+ "AsyncExternalStorageWithRawResponse",
+ "ExternalStorageWithStreamingResponse",
+ "AsyncExternalStorageWithStreamingResponse",
"SpendLimit",
"AsyncSpendLimit",
"SpendLimitWithRawResponse",
diff --git a/src/openai/resources/admin/organization/organization.py b/src/openai/resources/admin/organization/organization.py
index 0f2bf69fb3..f9d95942bd 100644
--- a/src/openai/resources/admin/organization/organization.py
+++ b/src/openai/resources/admin/organization/organization.py
@@ -113,10 +113,6 @@
class Organization(SyncAPIResource):
- @cached_property
- def external_storage(self) -> ExternalStorage:
- return ExternalStorage(self._client)
-
@cached_property
def audit_logs(self) -> AuditLogs:
"""List user actions and configuration changes within this organization."""
@@ -150,6 +146,10 @@ def roles(self) -> Roles:
def data_retention(self) -> DataRetention:
return DataRetention(self._client)
+ @cached_property
+ def external_storage(self) -> ExternalStorage:
+ return ExternalStorage(self._client)
+
@cached_property
def spend_limit(self) -> SpendLimit:
return SpendLimit(self._client)
@@ -187,10 +187,6 @@ def with_streaming_response(self) -> OrganizationWithStreamingResponse:
class AsyncOrganization(AsyncAPIResource):
- @cached_property
- def external_storage(self) -> AsyncExternalStorage:
- return AsyncExternalStorage(self._client)
-
@cached_property
def audit_logs(self) -> AsyncAuditLogs:
"""List user actions and configuration changes within this organization."""
@@ -224,6 +220,10 @@ def roles(self) -> AsyncRoles:
def data_retention(self) -> AsyncDataRetention:
return AsyncDataRetention(self._client)
+ @cached_property
+ def external_storage(self) -> AsyncExternalStorage:
+ return AsyncExternalStorage(self._client)
+
@cached_property
def spend_limit(self) -> AsyncSpendLimit:
return AsyncSpendLimit(self._client)
@@ -264,10 +264,6 @@ class OrganizationWithRawResponse:
def __init__(self, organization: Organization) -> None:
self._organization = organization
- @cached_property
- def external_storage(self) -> ExternalStorageWithRawResponse:
- return ExternalStorageWithRawResponse(self._organization.external_storage)
-
@cached_property
def audit_logs(self) -> AuditLogsWithRawResponse:
"""List user actions and configuration changes within this organization."""
@@ -301,6 +297,10 @@ def roles(self) -> RolesWithRawResponse:
def data_retention(self) -> DataRetentionWithRawResponse:
return DataRetentionWithRawResponse(self._organization.data_retention)
+ @cached_property
+ def external_storage(self) -> ExternalStorageWithRawResponse:
+ return ExternalStorageWithRawResponse(self._organization.external_storage)
+
@cached_property
def spend_limit(self) -> SpendLimitWithRawResponse:
return SpendLimitWithRawResponse(self._organization.spend_limit)
@@ -322,10 +322,6 @@ class AsyncOrganizationWithRawResponse:
def __init__(self, organization: AsyncOrganization) -> None:
self._organization = organization
- @cached_property
- def external_storage(self) -> AsyncExternalStorageWithRawResponse:
- return AsyncExternalStorageWithRawResponse(self._organization.external_storage)
-
@cached_property
def audit_logs(self) -> AsyncAuditLogsWithRawResponse:
"""List user actions and configuration changes within this organization."""
@@ -359,6 +355,10 @@ def roles(self) -> AsyncRolesWithRawResponse:
def data_retention(self) -> AsyncDataRetentionWithRawResponse:
return AsyncDataRetentionWithRawResponse(self._organization.data_retention)
+ @cached_property
+ def external_storage(self) -> AsyncExternalStorageWithRawResponse:
+ return AsyncExternalStorageWithRawResponse(self._organization.external_storage)
+
@cached_property
def spend_limit(self) -> AsyncSpendLimitWithRawResponse:
return AsyncSpendLimitWithRawResponse(self._organization.spend_limit)
@@ -380,10 +380,6 @@ class OrganizationWithStreamingResponse:
def __init__(self, organization: Organization) -> None:
self._organization = organization
- @cached_property
- def external_storage(self) -> ExternalStorageWithStreamingResponse:
- return ExternalStorageWithStreamingResponse(self._organization.external_storage)
-
@cached_property
def audit_logs(self) -> AuditLogsWithStreamingResponse:
"""List user actions and configuration changes within this organization."""
@@ -417,6 +413,10 @@ def roles(self) -> RolesWithStreamingResponse:
def data_retention(self) -> DataRetentionWithStreamingResponse:
return DataRetentionWithStreamingResponse(self._organization.data_retention)
+ @cached_property
+ def external_storage(self) -> ExternalStorageWithStreamingResponse:
+ return ExternalStorageWithStreamingResponse(self._organization.external_storage)
+
@cached_property
def spend_limit(self) -> SpendLimitWithStreamingResponse:
return SpendLimitWithStreamingResponse(self._organization.spend_limit)
@@ -438,10 +438,6 @@ class AsyncOrganizationWithStreamingResponse:
def __init__(self, organization: AsyncOrganization) -> None:
self._organization = organization
- @cached_property
- def external_storage(self) -> AsyncExternalStorageWithStreamingResponse:
- return AsyncExternalStorageWithStreamingResponse(self._organization.external_storage)
-
@cached_property
def audit_logs(self) -> AsyncAuditLogsWithStreamingResponse:
"""List user actions and configuration changes within this organization."""
@@ -475,6 +471,10 @@ def roles(self) -> AsyncRolesWithStreamingResponse:
def data_retention(self) -> AsyncDataRetentionWithStreamingResponse:
return AsyncDataRetentionWithStreamingResponse(self._organization.data_retention)
+ @cached_property
+ def external_storage(self) -> AsyncExternalStorageWithStreamingResponse:
+ return AsyncExternalStorageWithStreamingResponse(self._organization.external_storage)
+
@cached_property
def spend_limit(self) -> AsyncSpendLimitWithStreamingResponse:
return AsyncSpendLimitWithStreamingResponse(self._organization.spend_limit)
diff --git a/src/openai/resources/audio/transcriptions.py b/src/openai/resources/audio/transcriptions.py
index f5ec09441d..06d7f0667c 100644
--- a/src/openai/resources/audio/transcriptions.py
+++ b/src/openai/resources/audio/transcriptions.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import json
import logging
from typing import List, Union, Mapping, Optional, cast
from typing_extensions import Literal, overload
@@ -21,7 +22,7 @@
omit,
not_given,
)
-from ..._utils import extract_files, required_args, maybe_transform, async_maybe_transform
+from ..._utils import is_mapping, extract_files, required_args, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -92,7 +93,8 @@ def create(
Transcribes audio into the input language.
Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
- format, or a stream of transcript events.
+ format, plain text in `text`, `srt`, or `vtt` format, or a stream of transcript
+ events. Supported formats depend on the model.
Args:
file: The audio file object (not file name) to transcribe, in one of these formats:
@@ -260,7 +262,8 @@ def create(
Transcribes audio into the input language.
Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
- format, or a stream of transcript events.
+ format, plain text in `text`, `srt`, or `vtt` format, or a stream of transcript
+ events. Supported formats depend on the model.
Args:
file: The audio file object (not file name) to transcribe, in one of these formats:
@@ -381,7 +384,8 @@ def create(
Transcribes audio into the input language.
Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
- format, or a stream of transcript events.
+ format, plain text in `text`, `srt`, or `vtt` format, or a stream of transcript
+ events. Supported formats depend on the model.
Args:
file: The audio file object (not file name) to transcribe, in one of these formats:
@@ -498,7 +502,7 @@ def create(
extra_body: Body | None = None,
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> str | Transcription | TranscriptionDiarized | TranscriptionVerbose | Stream[TranscriptionStreamEvent]:
- body = deepcopy_with_paths(
+ body: dict[str, object] = deepcopy_with_paths(
{
"file": file,
"model": model,
@@ -517,6 +521,7 @@ def create(
},
[["file"]],
)
+ extra_body = _serialize_chunking_strategy(body, extra_body)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
@@ -595,7 +600,8 @@ async def create(
Transcribes audio into the input language.
Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
- format, or a stream of transcript events.
+ format, plain text in `text`, `srt`, or `vtt` format, or a stream of transcript
+ events. Supported formats depend on the model.
Args:
file: The audio file object (not file name) to transcribe, in one of these formats:
@@ -728,6 +734,30 @@ async def create(
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> str: ...
+ @overload
+ async def create(
+ self,
+ *,
+ file: FileTypes,
+ model: Union[str, AudioModel],
+ chunking_strategy: Optional[transcription_create_params.ChunkingStrategy] | Omit = omit,
+ response_format: Literal["diarized_json"],
+ stream: Optional[Literal[False]] | Omit = omit,
+ keywords: SequenceNotStr[str] | Omit = omit,
+ known_speaker_names: SequenceNotStr[str] | Omit = omit,
+ known_speaker_references: SequenceNotStr[str] | Omit = omit,
+ language: str | Omit = omit,
+ languages: SequenceNotStr[str] | Omit = omit,
+ temperature: float | Omit = omit,
+ timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx2.Timeout | None | NotGiven = not_given,
+ ) -> TranscriptionDiarized: ...
+
@overload
async def create(
self,
@@ -757,7 +787,8 @@ async def create(
Transcribes audio into the input language.
Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
- format, or a stream of transcript events.
+ format, plain text in `text`, `srt`, or `vtt` format, or a stream of transcript
+ events. Supported formats depend on the model.
Args:
file: The audio file object (not file name) to transcribe, in one of these formats:
@@ -878,7 +909,8 @@ async def create(
Transcribes audio into the input language.
Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
- format, or a stream of transcript events.
+ format, plain text in `text`, `srt`, or `vtt` format, or a stream of transcript
+ events. Supported formats depend on the model.
Args:
file: The audio file object (not file name) to transcribe, in one of these formats:
@@ -995,7 +1027,7 @@ async def create(
extra_body: Body | None = None,
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> Transcription | TranscriptionVerbose | TranscriptionDiarized | str | AsyncStream[TranscriptionStreamEvent]:
- body = deepcopy_with_paths(
+ body: dict[str, object] = deepcopy_with_paths(
{
"file": file,
"model": model,
@@ -1014,6 +1046,7 @@ async def create(
},
[["file"]],
)
+ extra_body = _serialize_chunking_strategy(body, extra_body)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
@@ -1041,6 +1074,18 @@ async def create(
)
+def _serialize_chunking_strategy(body: dict[str, object], extra_body: Body | None) -> Body | None:
+ # The service expects one JSON-text field. Normalize only the winning value.
+ if is_mapping(extra_body) and "chunking_strategy" in extra_body:
+ body.pop("chunking_strategy", None)
+ strategy = extra_body["chunking_strategy"]
+ if isinstance(strategy, dict):
+ return {**extra_body, "chunking_strategy": json.dumps(strategy)}
+ elif isinstance(body.get("chunking_strategy"), dict):
+ body["chunking_strategy"] = json.dumps(body["chunking_strategy"])
+ return extra_body
+
+
class TranscriptionsWithRawResponse:
def __init__(self, transcriptions: Transcriptions) -> None:
self._transcriptions = transcriptions
diff --git a/src/openai/resources/safety/__init__.py b/src/openai/resources/safety/__init__.py
index 57d50e6bc3..c25334f1b4 100644
--- a/src/openai/resources/safety/__init__.py
+++ b/src/openai/resources/safety/__init__.py
@@ -30,18 +30,18 @@
else:
_EXPORTS = {
- "Alerts": (".alerts", "Alerts"),
- "AsyncAlerts": (".alerts", "AsyncAlerts"),
- "AlertsWithRawResponse": (".alerts", "AlertsWithRawResponse"),
- "AsyncAlertsWithRawResponse": (".alerts", "AsyncAlertsWithRawResponse"),
- "AlertsWithStreamingResponse": (".alerts", "AlertsWithStreamingResponse"),
- "AsyncAlertsWithStreamingResponse": (".alerts", "AsyncAlertsWithStreamingResponse"),
"Cases": (".cases", "Cases"),
"AsyncCases": (".cases", "AsyncCases"),
"CasesWithRawResponse": (".cases", "CasesWithRawResponse"),
"AsyncCasesWithRawResponse": (".cases", "AsyncCasesWithRawResponse"),
"CasesWithStreamingResponse": (".cases", "CasesWithStreamingResponse"),
"AsyncCasesWithStreamingResponse": (".cases", "AsyncCasesWithStreamingResponse"),
+ "Alerts": (".alerts", "Alerts"),
+ "AsyncAlerts": (".alerts", "AsyncAlerts"),
+ "AlertsWithRawResponse": (".alerts", "AlertsWithRawResponse"),
+ "AsyncAlertsWithRawResponse": (".alerts", "AsyncAlertsWithRawResponse"),
+ "AlertsWithStreamingResponse": (".alerts", "AlertsWithStreamingResponse"),
+ "AsyncAlertsWithStreamingResponse": (".alerts", "AsyncAlertsWithStreamingResponse"),
"Safety": (".safety", "Safety"),
"AsyncSafety": (".safety", "AsyncSafety"),
"SafetyWithRawResponse": (".safety", "SafetyWithRawResponse"),
@@ -73,18 +73,18 @@ def __dir__() -> list[str]:
__all__ = [
- "Alerts",
- "AsyncAlerts",
- "AlertsWithRawResponse",
- "AsyncAlertsWithRawResponse",
- "AlertsWithStreamingResponse",
- "AsyncAlertsWithStreamingResponse",
"Cases",
"AsyncCases",
"CasesWithRawResponse",
"AsyncCasesWithRawResponse",
"CasesWithStreamingResponse",
"AsyncCasesWithStreamingResponse",
+ "Alerts",
+ "AsyncAlerts",
+ "AlertsWithRawResponse",
+ "AsyncAlertsWithRawResponse",
+ "AlertsWithStreamingResponse",
+ "AsyncAlertsWithStreamingResponse",
"Safety",
"AsyncSafety",
"SafetyWithRawResponse",
diff --git a/src/openai/resources/safety/safety.py b/src/openai/resources/safety/safety.py
index 4d80f73eda..ae72977ba4 100644
--- a/src/openai/resources/safety/safety.py
+++ b/src/openai/resources/safety/safety.py
@@ -25,14 +25,14 @@
class Safety(SyncAPIResource):
- @cached_property
- def alerts(self) -> Alerts:
- return Alerts(self._client)
-
@cached_property
def cases(self) -> Cases:
return Cases(self._client)
+ @cached_property
+ def alerts(self) -> Alerts:
+ return Alerts(self._client)
+
@cached_property
def with_raw_response(self) -> SafetyWithRawResponse:
"""
@@ -54,14 +54,14 @@ def with_streaming_response(self) -> SafetyWithStreamingResponse:
class AsyncSafety(AsyncAPIResource):
- @cached_property
- def alerts(self) -> AsyncAlerts:
- return AsyncAlerts(self._client)
-
@cached_property
def cases(self) -> AsyncCases:
return AsyncCases(self._client)
+ @cached_property
+ def alerts(self) -> AsyncAlerts:
+ return AsyncAlerts(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncSafetyWithRawResponse:
"""
@@ -86,49 +86,49 @@ class SafetyWithRawResponse:
def __init__(self, safety: Safety) -> None:
self._safety = safety
- @cached_property
- def alerts(self) -> AlertsWithRawResponse:
- return AlertsWithRawResponse(self._safety.alerts)
-
@cached_property
def cases(self) -> CasesWithRawResponse:
return CasesWithRawResponse(self._safety.cases)
+ @cached_property
+ def alerts(self) -> AlertsWithRawResponse:
+ return AlertsWithRawResponse(self._safety.alerts)
+
class AsyncSafetyWithRawResponse:
def __init__(self, safety: AsyncSafety) -> None:
self._safety = safety
- @cached_property
- def alerts(self) -> AsyncAlertsWithRawResponse:
- return AsyncAlertsWithRawResponse(self._safety.alerts)
-
@cached_property
def cases(self) -> AsyncCasesWithRawResponse:
return AsyncCasesWithRawResponse(self._safety.cases)
+ @cached_property
+ def alerts(self) -> AsyncAlertsWithRawResponse:
+ return AsyncAlertsWithRawResponse(self._safety.alerts)
+
class SafetyWithStreamingResponse:
def __init__(self, safety: Safety) -> None:
self._safety = safety
- @cached_property
- def alerts(self) -> AlertsWithStreamingResponse:
- return AlertsWithStreamingResponse(self._safety.alerts)
-
@cached_property
def cases(self) -> CasesWithStreamingResponse:
return CasesWithStreamingResponse(self._safety.cases)
+ @cached_property
+ def alerts(self) -> AlertsWithStreamingResponse:
+ return AlertsWithStreamingResponse(self._safety.alerts)
+
class AsyncSafetyWithStreamingResponse:
def __init__(self, safety: AsyncSafety) -> None:
self._safety = safety
- @cached_property
- def alerts(self) -> AsyncAlertsWithStreamingResponse:
- return AsyncAlertsWithStreamingResponse(self._safety.alerts)
-
@cached_property
def cases(self) -> AsyncCasesWithStreamingResponse:
return AsyncCasesWithStreamingResponse(self._safety.cases)
+
+ @cached_property
+ def alerts(self) -> AsyncAlertsWithStreamingResponse:
+ return AsyncAlertsWithStreamingResponse(self._safety.alerts)
diff --git a/tests/lib/test_audio_diarized_types.py b/tests/lib/test_audio_diarized_types.py
new file mode 100644
index 0000000000..d423b8799e
--- /dev/null
+++ b/tests/lib/test_audio_diarized_types.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from typing_extensions import assert_type
+
+import httpx2
+import pytest
+
+from openai import AsyncOpenAI, omit
+from tests.respx2 import MockRouter
+from openai.types.audio import TranscriptionDiarized, TranscriptionDiarizedSegment
+
+
+@pytest.mark.respx2()
+async def test_async_diarized_nonstreaming_types(async_client: AsyncOpenAI, respx2_mock: MockRouter) -> None:
+ route = respx2_mock.post(f"{str(async_client.base_url).rstrip('/')}/audio/transcriptions").mock(
+ return_value=httpx2.Response(
+ 200,
+ json={
+ "text": "hello",
+ "duration": 1.0,
+ "task": "transcribe",
+ "segments": [
+ {
+ "id": "segment-1",
+ "type": "transcript.text.segment",
+ "start": 0.0,
+ "end": 1.0,
+ "text": "hello",
+ "speaker": "A",
+ }
+ ],
+ },
+ )
+ )
+ default = assert_type(
+ await async_client.audio.transcriptions.create(
+ file=b"audio", model="gpt-4o-transcribe-diarize", response_format="diarized_json"
+ ),
+ TranscriptionDiarized,
+ )
+ false = assert_type(
+ await async_client.audio.transcriptions.create(
+ file=b"audio", model="gpt-4o-transcribe-diarize", response_format="diarized_json", stream=False
+ ),
+ TranscriptionDiarized,
+ )
+ null = assert_type(
+ await async_client.audio.transcriptions.create(
+ file=b"audio", model="gpt-4o-transcribe-diarize", response_format="diarized_json", stream=None
+ ),
+ TranscriptionDiarized,
+ )
+ omitted = assert_type(
+ await async_client.audio.transcriptions.create(
+ file=b"audio", model="gpt-4o-transcribe-diarize", response_format="diarized_json", stream=omit
+ ),
+ TranscriptionDiarized,
+ )
+ for response in (default, false, null, omitted):
+ assert isinstance(response, TranscriptionDiarized)
+ assert_type(response.segments[0], TranscriptionDiarizedSegment)
+ assert response.segments[0].id == "segment-1"
+ assert response.segments[0].speaker == "A"
+ assert route.call_count == 4
diff --git a/tests/lib/test_audio_transcription_multipart.py b/tests/lib/test_audio_transcription_multipart.py
new file mode 100644
index 0000000000..155dd83358
--- /dev/null
+++ b/tests/lib/test_audio_transcription_multipart.py
@@ -0,0 +1,148 @@
+from __future__ import annotations
+
+import io
+import json
+from copy import deepcopy
+from email import policy
+from typing import Any, cast
+from email.parser import BytesParser
+
+import httpx2
+import pytest
+
+from openai import OpenAI, AsyncOpenAI, omit
+from openai.types.audio import Transcription
+
+FILE_BYTES = b"RIFF\x00synthetic audio\xff\r\nWAVE"
+VAD = {"type": "server_vad", "threshold": 0, "prefix_padding_ms": 0, "silence_duration_ms": 0}
+UNSERIALIZABLE = object()
+CASES = [
+ pytest.param({"chunking_strategy": VAD}, VAD, id="vad-zero"),
+ pytest.param({"chunking_strategy": {"type": "server_vad"}}, {"type": "server_vad"}, id="vad-defaults"),
+ pytest.param({"chunking_strategy": {**VAD, "future": True}}, {**VAD, "future": True}, id="extra-object-field"),
+ pytest.param(
+ {"chunking_strategy": "auto", "extra_body": {"chunking_strategy": VAD, "extra_flag": False}},
+ VAD,
+ id="override-vad",
+ ),
+ pytest.param(
+ {"chunking_strategy": {"unserializable": UNSERIALIZABLE}, "extra_body": {"chunking_strategy": VAD}},
+ VAD,
+ id="override-losing-object",
+ ),
+ pytest.param(
+ {"chunking_strategy": {"unserializable": UNSERIALIZABLE}, "extra_body": {"chunking_strategy": "auto"}},
+ "auto",
+ id="override-auto",
+ ),
+ pytest.param({"chunking_strategy": VAD, "extra_body": {"chunking_strategy": None}}, None, id="override-null"),
+ pytest.param({"chunking_strategy": VAD, "extra_body": {"chunking_strategy": omit}}, None, id="override-omit"),
+ pytest.param({"chunking_strategy": "auto"}, "auto", id="auto"),
+ pytest.param({}, None, id="omitted"),
+ pytest.param({"chunking_strategy": None}, None, id="null"),
+ pytest.param({"chunking_strategy": omit}, None, id="omit"),
+ pytest.param({"chunking_strategy": {}}, {}, id="empty-object"),
+ pytest.param({"extra_body": {"chunking_strategy": 0}}, "0", id="scalar-zero"),
+ pytest.param({"extra_body": {"chunking_strategy": False}}, "false", id="scalar-false"),
+ pytest.param({"extra_body": {"chunking_strategy": json.dumps(VAD)}}, VAD, id="preencoded-json"),
+ pytest.param({"chunking_strategy": VAD, "extra_body": {"extra_flag": False}}, VAD, id="unrelated-override"),
+]
+
+
+@pytest.fixture(autouse=True)
+def clean_sdk_environment(monkeypatch: pytest.MonkeyPatch) -> None:
+ import os
+
+ for key in os.environ:
+ if key.startswith(("OPENAI_", "AZURE_OPENAI_")):
+ monkeypatch.delenv(key)
+
+
+def assert_multipart(request: httpx2.Request, expected: object, *, has_extra_flag: bool) -> None:
+ assert request.url.path == "/v1/audio/transcriptions"
+ assert request.headers["x-request-marker"] == "request"
+ assert request.url.params["probe"] == "request"
+ content_type = request.headers["content-type"]
+ assert content_type.startswith("multipart/form-data; boundary=")
+ message = BytesParser(policy=policy.default).parsebytes(
+ f"Content-Type: {content_type}\r\nMIME-Version: 1.0\r\n\r\n".encode() + request.read()
+ )
+ fields: dict[str, list[bytes]] = {}
+ files: list[tuple[str, str | None, str, bytes]] = []
+ for part in message.iter_parts():
+ name = part.get_param("name", header="content-disposition")
+ assert isinstance(name, str)
+ payload = part.get_payload(decode=True)
+ assert isinstance(payload, bytes)
+ if part.get_filename() is not None:
+ files.append((name, part.get_filename(), part.get_content_type(), payload))
+ else:
+ fields.setdefault(name, []).append(payload)
+
+ assert files == [("file", "test.wav", "audio/wav", FILE_BYTES)]
+ assert fields["model"] == [b"gpt-4o-transcribe"]
+ assert fields["temperature"] == [b"0"]
+ assert fields["stream"] == [b"false"]
+ assert fields["include[]"] == [b"logprobs"]
+ assert fields["timestamp_granularities[]"] == [b"word", b"segment"]
+ assert fields["known_speaker_names[]"] == [b"Alice", b"Bob"]
+ assert fields["known_speaker_references[]"] == [b"data:audio/wav;base64,AA==", b"data:audio/wav;base64,AQ=="]
+ assert fields.get("extra_flag", []) == ([b"false"] if has_extra_flag else [])
+ assert not any(name.startswith("chunking_strategy[") for name in fields)
+ values = fields.get("chunking_strategy", [])
+ if isinstance(expected, dict):
+ assert len(values) == 1
+ assert json.loads(values[0]) == expected
+ else:
+ assert values == ([] if expected is None else [str(expected).encode()])
+
+
+@pytest.mark.parametrize("changes,expected", CASES)
+@pytest.mark.parametrize("async_client", [False, True], ids=["sync", "async"])
+async def test_chunking_strategy_wire(changes: dict[str, Any], expected: object, async_client: bool) -> None:
+ changes = deepcopy(changes, {id(UNSERIALIZABLE): UNSERIALIZABLE, id(omit): omit})
+ before = deepcopy(changes, {id(UNSERIALIZABLE): UNSERIALIZABLE, id(omit): omit})
+ requests: list[httpx2.Request] = []
+
+ def handler(request: httpx2.Request) -> httpx2.Response:
+ request.read()
+ requests.append(request)
+ return httpx2.Response(200, json={"text": "synthetic"})
+
+ common: dict[str, Any] = {
+ "api_key": "sk-synthetic",
+ "base_url": "https://multipart.invalid/v1",
+ "max_retries": 0,
+ "default_headers": {"x-request-marker": "client"},
+ "default_query": {"probe": "client"},
+ }
+ with io.BytesIO(FILE_BYTES) as file:
+ kwargs: dict[str, Any] = {
+ "file": ("test.wav", file, "audio/wav"),
+ "model": "gpt-4o-transcribe",
+ "temperature": 0,
+ "stream": False,
+ "include": ["logprobs"],
+ "timestamp_granularities": ["word", "segment"],
+ "known_speaker_names": ["Alice", "Bob"],
+ "known_speaker_references": ["data:audio/wav;base64,AA==", "data:audio/wav;base64,AQ=="],
+ "extra_headers": {"x-request-marker": "request"},
+ "extra_query": {"probe": "request"},
+ **changes,
+ }
+ if async_client:
+ async with AsyncOpenAI(
+ **common, http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handler), trust_env=False)
+ ) as client:
+ response = cast(Transcription, await client.audio.transcriptions.create(**kwargs))
+ assert response.text == "synthetic"
+ else:
+ with OpenAI(
+ **common, http_client=httpx2.Client(transport=httpx2.MockTransport(handler), trust_env=False)
+ ) as sync_client:
+ response = cast(Transcription, sync_client.audio.transcriptions.create(**kwargs))
+ assert response.text == "synthetic"
+ assert not file.closed
+ assert changes == before
+ assert len(requests) == 1
+ assert_multipart(requests[0], expected, has_extra_flag="extra_flag" in changes.get("extra_body", {}))