Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/openai/lib/_parsing/_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING
from typing_extensions import assert_never

from ..._types import Omit
from ...types.audio.translation import Translation
from ...types.audio.transcription import Transcription
from ...types.audio_response_format import AudioResponseFormat
from ...types.audio.translation_verbose import TranslationVerbose
from ...types.audio.transcription_verbose import TranscriptionVerbose
from ...types.audio.transcription_diarized import TranscriptionDiarized


def get_transcription_response_format_type(
response_format: AudioResponseFormat | Omit,
*,
log: logging.Logger,
) -> type[Transcription | TranscriptionVerbose | TranscriptionDiarized | str]:
if isinstance(response_format, Omit) or response_format is None: # pyright: ignore[reportUnnecessaryComparison]
return Transcription

if response_format == "json":
return Transcription
elif response_format == "verbose_json":
return TranscriptionVerbose
elif response_format == "diarized_json":
return TranscriptionDiarized
elif response_format == "srt" or response_format == "text" or response_format == "vtt":
return str
elif TYPE_CHECKING: # type: ignore[unreachable]
assert_never(response_format)
else:
log.warn("Unexpected audio response format: %s", response_format)
return Transcription


def get_translation_response_format_type(
response_format: AudioResponseFormat | Omit,
*,
log: logging.Logger,
) -> type[Translation | TranslationVerbose | str]:
if isinstance(response_format, Omit) or response_format is None: # pyright: ignore[reportUnnecessaryComparison]
return Translation

if response_format == "json":
return Translation
elif response_format == "verbose_json":
return TranslationVerbose
elif response_format == "srt" or response_format == "text" or response_format == "vtt":
return str
elif TYPE_CHECKING and response_format != "diarized_json": # type: ignore[unreachable]
assert_never(response_format)
else:
log.warning("Unexpected audio response format: %s", response_format)
return Translation
35 changes: 35 additions & 0 deletions src/openai/lib/_parsing/_embeddings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import annotations

import array
import base64
from typing import cast

from ..._types import Omit, NotGiven
from ..._utils import is_given
from ..._extras import numpy as np, has_numpy
from ...types.create_embedding_response import CreateEmbeddingResponse


def parse_embedding_response(
obj: CreateEmbeddingResponse, *, encoding_format: str | Omit | NotGiven
) -> CreateEmbeddingResponse:
if is_given(encoding_format):
# don't modify the response object if a user explicitly asked for a format
return obj

if not obj.data:
raise ValueError("No embedding data received")

for embedding in obj.data:
data = cast(object, embedding.embedding)
if not isinstance(data, str):
continue
if not has_numpy():
# use array for base64 optimisation
embedding.embedding = array.array("f", base64.b64decode(data)).tolist()
else:
embedding.embedding = np.frombuffer( # type: ignore[no-untyped-call]
base64.b64decode(data), dtype="float32"
).tolist()

return obj
64 changes: 64 additions & 0 deletions src/openai/lib/_webhooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from __future__ import annotations

import hmac
import time
import base64
import hashlib

from .._types import HeadersLike
from .._utils import get_required_header
from .._exceptions import InvalidWebhookSignatureError


def webhook_signature_matches(
payload: str | bytes,
headers: HeadersLike,
*,
secret: str,
tolerance: int,
) -> bool:
"""Validate the replay window and compare the supplied signatures."""
signature_header = get_required_header(headers, "webhook-signature")
timestamp = get_required_header(headers, "webhook-timestamp")
webhook_id = get_required_header(headers, "webhook-id")

# Validate timestamp to prevent replay attacks
try:
timestamp_seconds = int(timestamp)
except ValueError:
raise InvalidWebhookSignatureError("Invalid webhook timestamp format") from None

now = int(time.time())

if now - timestamp_seconds > tolerance:
raise InvalidWebhookSignatureError("Webhook timestamp is too old") from None

if timestamp_seconds > now + tolerance:
raise InvalidWebhookSignatureError("Webhook timestamp is too new") from None

# Extract signatures from v1,<base64> format
# The signature header can have multiple values, separated by spaces.
# Each value is in the format v1,<base64>. We should accept if any match.
signatures: list[str] = []
for part in signature_header.split():
if part.startswith("v1,"):
signatures.append(part[3:])
else:
signatures.append(part)

# Decode the secret if it starts with whsec_
if secret.startswith("whsec_"):
decoded_secret = base64.b64decode(secret[6:])
else:
decoded_secret = secret.encode()

body = payload.decode("utf-8") if isinstance(payload, bytes) else payload

# Prepare the signed payload (OpenAI uses webhookId.timestamp.payload format)
signed_payload = f"{webhook_id}.{timestamp}.{body}"
expected_signature = base64.b64encode(
hmac.new(decoded_secret, signed_payload.encode(), hashlib.sha256).digest()
).decode()

# Accept if any signature matches
return any(hmac.compare_digest(expected_signature, sig) for sig in signatures)
22 changes: 4 additions & 18 deletions src/openai/resources/audio/transcriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, List, Union, Mapping, Optional, cast
from typing_extensions import Literal, overload, assert_never
from typing import List, Union, Mapping, Optional, cast
from typing_extensions import Literal, overload

import httpx2

Expand All @@ -29,6 +29,7 @@
from ...types.audio import transcription_create_params
from ..._base_client import make_request_options
from ...types.audio_model import AudioModel
from ...lib._parsing._audio import get_transcription_response_format_type as _get_transcription_response_format_type
from ...types.audio.transcription import Transcription
from ...types.audio_response_format import AudioResponseFormat
from ...types.audio.transcription_include import TranscriptionInclude
Expand Down Expand Up @@ -1078,19 +1079,4 @@ def __init__(self, transcriptions: AsyncTranscriptions) -> None:
def _get_response_format_type(
response_format: AudioResponseFormat | Omit,
) -> type[Transcription | TranscriptionVerbose | TranscriptionDiarized | str]:
if isinstance(response_format, Omit) or response_format is None: # pyright: ignore[reportUnnecessaryComparison]
return Transcription

if response_format == "json":
return Transcription
elif response_format == "verbose_json":
return TranscriptionVerbose
elif response_format == "diarized_json":
return TranscriptionDiarized
elif response_format == "srt" or response_format == "text" or response_format == "vtt":
return str
elif TYPE_CHECKING: # type: ignore[unreachable]
assert_never(response_format)
else:
log.warn("Unexpected audio response format: %s", response_format)
return Transcription
return _get_transcription_response_format_type(response_format, log=log)
20 changes: 4 additions & 16 deletions src/openai/resources/audio/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Union, Mapping, cast
from typing_extensions import Literal, overload, assert_never
from typing import Union, Mapping, cast
from typing_extensions import Literal, overload

import httpx2

Expand All @@ -18,6 +18,7 @@
from ...types.audio import translation_create_params
from ..._base_client import make_request_options
from ...types.audio_model import AudioModel
from ...lib._parsing._audio import get_translation_response_format_type as _get_translation_response_format_type
from ...types.audio.translation import Translation
from ...types.audio_response_format import AudioResponseFormat
from ...types.audio.translation_verbose import TranslationVerbose
Expand Down Expand Up @@ -370,17 +371,4 @@ def __init__(self, translations: AsyncTranslations) -> None:
def _get_response_format_type(
response_format: AudioResponseFormat | Omit,
) -> type[Translation | TranslationVerbose | str]:
if isinstance(response_format, Omit) or response_format is None: # pyright: ignore[reportUnnecessaryComparison]
return Translation

if response_format == "json":
return Translation
elif response_format == "verbose_json":
return TranslationVerbose
elif response_format == "srt" or response_format == "text" or response_format == "vtt":
return str
elif TYPE_CHECKING and response_format != "diarized_json": # type: ignore[unreachable]
assert_never(response_format)
else:
log.warning("Unexpected audio response format: %s", response_format)
return Translation
return _get_translation_response_format_type(response_format, log=log)
55 changes: 5 additions & 50 deletions src/openai/resources/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

from __future__ import annotations

import array
import base64
from typing import Union, Iterable, cast
from typing import Union, Iterable
from functools import partial
from typing_extensions import Literal

import httpx2
Expand All @@ -14,11 +13,11 @@
from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from .._utils import is_given, maybe_transform
from .._compat import cached_property
from .._extras import numpy as np, has_numpy
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
from .._base_client import make_request_options
from ..types.embedding_model import EmbeddingModel
from ..lib._parsing._embeddings import parse_embedding_response as _parse_embedding_response
from ..types.create_embedding_response import CreateEmbeddingResponse

__all__ = ["Embeddings", "AsyncEmbeddings"]
Expand Down Expand Up @@ -111,28 +110,6 @@ def create(
if not is_given(encoding_format):
params["encoding_format"] = "base64"

def parser(obj: CreateEmbeddingResponse) -> CreateEmbeddingResponse:
if is_given(encoding_format):
# don't modify the response object if a user explicitly asked for a format
return obj

if not obj.data:
raise ValueError("No embedding data received")

for embedding in obj.data:
data = cast(object, embedding.embedding)
if not isinstance(data, str):
continue
if not has_numpy():
# use array for base64 optimisation
embedding.embedding = array.array("f", base64.b64decode(data)).tolist()
else:
embedding.embedding = np.frombuffer( # type: ignore[no-untyped-call]
base64.b64decode(data), dtype="float32"
).tolist()

return obj

return self._post(
"/embeddings",
body=maybe_transform(params, embedding_create_params.EmbeddingCreateParams),
Expand All @@ -141,7 +118,7 @@ def parser(obj: CreateEmbeddingResponse) -> CreateEmbeddingResponse:
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
post_parser=parser,
post_parser=partial(_parse_embedding_response, encoding_format=encoding_format),
security={"bearer_auth": True},
),
cast_to=CreateEmbeddingResponse,
Expand Down Expand Up @@ -235,28 +212,6 @@ async def create(
if not is_given(encoding_format):
params["encoding_format"] = "base64"

def parser(obj: CreateEmbeddingResponse) -> CreateEmbeddingResponse:
if is_given(encoding_format):
# don't modify the response object if a user explicitly asked for a format
return obj

if not obj.data:
raise ValueError("No embedding data received")

for embedding in obj.data:
data = cast(object, embedding.embedding)
if not isinstance(data, str):
continue
if not has_numpy():
# use array for base64 optimisation
embedding.embedding = array.array("f", base64.b64decode(data)).tolist()
else:
embedding.embedding = np.frombuffer( # type: ignore[no-untyped-call]
base64.b64decode(data), dtype="float32"
).tolist()

return obj

return await self._post(
"/embeddings",
body=maybe_transform(params, embedding_create_params.EmbeddingCreateParams),
Expand All @@ -265,7 +220,7 @@ def parser(obj: CreateEmbeddingResponse) -> CreateEmbeddingResponse:
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
post_parser=parser,
post_parser=partial(_parse_embedding_response, encoding_format=encoding_format),
security={"bearer_auth": True},
),
cast_to=CreateEmbeddingResponse,
Expand Down
Loading
Loading