From 79a71680e2173ab28831aadf91e26f635aa4967f Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Mon, 21 Sep 2026 14:44:20 -0700 Subject: [PATCH] chore: regenerate PiperOrigin-RevId: 985523336 --- google/genai/_gaos/utils/__init__.py | 3 ++ google/genai/_gaos/utils/response_helpers.py | 5 +- google/genai/_gaos/utils/retries.py | 45 +++++++++-------- google/genai/_gaos/utils/serializers.py | 30 ++++++++++-- google/genai/_gaos/utils/unions.py | 51 ++++++++++++++------ 5 files changed, 92 insertions(+), 42 deletions(-) diff --git a/google/genai/_gaos/utils/__init__.py b/google/genai/_gaos/utils/__init__.py index 588cd98f5..89349a94d 100644 --- a/google/genai/_gaos/utils/__init__.py +++ b/google/genai/_gaos/utils/__init__.py @@ -53,6 +53,7 @@ async def run_sync_in_thread(func: Callable[..., _T], *args) -> _T: from .security import get_security, get_security_from_env from .serializers import ( + ALLOW_UNKNOWN_UNION_VARIANTS, get_pydantic_model, marshal_json, unmarshal, @@ -125,6 +126,7 @@ async def run_sync_in_thread(func: Callable[..., _T], *args) -> _T: "stream_to_bytes", "stream_to_bytes_async", "template_url", + "ALLOW_UNKNOWN_UNION_VARIANTS", "unmarshal", "unmarshal_json", "validate_decimal", @@ -147,6 +149,7 @@ async def run_sync_in_thread(func: Callable[..., _T], *args) -> _T: "parse_duration": ".datetimes", "get_global_from_env": ".values", "get_headers": ".headers", + "ALLOW_UNKNOWN_UNION_VARIANTS": ".serializers", "get_pydantic_model": ".serializers", "get_query_params": ".queryparams", "get_response_headers": ".headers", diff --git a/google/genai/_gaos/utils/response_helpers.py b/google/genai/_gaos/utils/response_helpers.py index 97f690922..bbb844d39 100644 --- a/google/genai/_gaos/utils/response_helpers.py +++ b/google/genai/_gaos/utils/response_helpers.py @@ -53,6 +53,7 @@ from .._version import __response_mode_header__ from .._hooks.types import AfterParseErrorContext from .eventstreaming import Stream, AsyncStream +from .serializers import ALLOW_UNKNOWN_UNION_VARIANTS from .unmarshal_json_response import unmarshal_json_response P = ParamSpec("P") @@ -207,7 +208,9 @@ def _synthesized_decoder(raw: str, _t: Any = chunk_t) -> Any: raise ValueError( f"Synthesized SSE decoder expected an envelope of shape {{'data': ...}}, got {envelope!r}. Pass decoder= to parse(...) to handle non-standard envelopes." ) - return _t.model_validate(envelope["data"]) + return _t.model_validate( + envelope["data"], context={ALLOW_UNKNOWN_UNION_VARIANTS: True} + ) resolved_decoder = _synthesized_decoder diff --git a/google/genai/_gaos/utils/retries.py b/google/genai/_gaos/utils/retries.py index 0b47124aa..1dc009996 100644 --- a/google/genai/_gaos/utils/retries.py +++ b/google/genai/_gaos/utils/retries.py @@ -26,17 +26,6 @@ import httpx -try: - import httpx2 -except ImportError: - httpx2 = None - -_RETRY_EXCEPTIONS = ( - (httpx.NetworkError, httpx.TimeoutException) - if httpx2 is None - else (httpx.NetworkError, httpx.TimeoutException, httpx2.NetworkError, httpx2.TimeoutException) -) - class BackoffStrategy: """Exponential backoff strategy configuration.""" @@ -142,6 +131,18 @@ def __init__(self, inner: Exception): self.inner = inner +_TRANSPORT_ERROR_NAMES = frozenset({"NetworkError", "TimeoutException"}) +_TRANSPORT_ERROR_BASES = frozenset({"TransportError", "RequestError", "HTTPError"}) + + +def _is_transport_error(exception: BaseException) -> bool: + """Report whether an exception is a connection or timeout failure.""" + if isinstance(exception, (httpx.NetworkError, httpx.TimeoutException)): + return True + names = {base.__name__ for base in type(exception).__mro__} + return bool(names & _TRANSPORT_ERROR_NAMES) and _TRANSPORT_ERROR_BASES <= names + + def _parse_retry_after_header(response: httpx.Response) -> Optional[int]: """Parse Retry-After header from response. @@ -248,14 +249,15 @@ def do_request(attempt: int) -> httpx.Response: if should_retry: raise TemporaryError(res) - except _RETRY_EXCEPTIONS as exception: - if retries.config.retry_connection_errors: - raise - - raise PermanentError(exception) from exception except TemporaryError: raise except Exception as exception: + if ( + _is_transport_error(exception) + and retries.config.retry_connection_errors + ): + raise + raise PermanentError(exception) from exception return res @@ -308,14 +310,15 @@ async def do_request(attempt: int) -> httpx.Response: if should_retry: raise TemporaryError(res) - except _RETRY_EXCEPTIONS as exception: - if retries.config.retry_connection_errors: - raise - - raise PermanentError(exception) from exception except TemporaryError: raise except Exception as exception: + if ( + _is_transport_error(exception) + and retries.config.retry_connection_errors + ): + raise + raise PermanentError(exception) from exception return res diff --git a/google/genai/_gaos/utils/serializers.py b/google/genai/_gaos/utils/serializers.py index cbfec2493..99747b4f6 100644 --- a/google/genai/_gaos/utils/serializers.py +++ b/google/genai/_gaos/utils/serializers.py @@ -129,11 +129,26 @@ def validate(c): return validate +ALLOW_UNKNOWN_UNION_VARIANTS = "speakeasy_allow_unknown_union_variants" +"""Validation-context key enabling the Unknown fallback on open discriminated +unions. The SDK sets it when deserializing server responses; validation +without it (e.g. of user-constructed request payloads) stays strict. Pass +``context={ALLOW_UNKNOWN_UNION_VARIANTS: True}`` to ``model_validate`` to +opt in when parsing response payloads manually.""" + + def unmarshal_json(raw, typ: Any) -> Any: - return unmarshal(from_json(raw), typ, coerce_iterables=False) + return unmarshal( + from_json(raw), typ, coerce_iterables=False, allow_unknown_union_variants=True + ) -def unmarshal(val, typ: Any, coerce_iterables: bool = True) -> Any: +def unmarshal( + val, + typ: Any, + coerce_iterables: bool = True, + allow_unknown_union_variants: bool = False, +) -> Any: if coerce_iterables: val = _coerce_iterables_for_type(val, typ) unmarshaller = create_model( @@ -142,7 +157,12 @@ def unmarshal(val, typ: Any, coerce_iterables: bool = True) -> Any: __config__=ConfigDict(populate_by_name=True, arbitrary_types_allowed=True), ) - m = unmarshaller(body=val) + if allow_unknown_union_variants: + m = unmarshaller.model_validate( + {"body": val}, context={ALLOW_UNKNOWN_UNION_VARIANTS: True} + ) + else: + m = unmarshaller(body=val) # pyright: ignore[reportAttributeAccessIssue] return m.body # type: ignore @@ -153,7 +173,9 @@ def unmarshal(val, typ: Any, coerce_iterables: bool = True) -> Any: def construct_unvalidated(value: Any, typ: Any, _depth: int = 0) -> Any: try: - return unmarshal(value, typ, coerce_iterables=True) + return unmarshal( + value, typ, coerce_iterables=True, allow_unknown_union_variants=True + ) except Exception: try: return _construct_lenient(value, typ, _depth) diff --git a/google/genai/_gaos/utils/unions.py b/google/genai/_gaos/utils/unions.py index ca2fc46de..a205b82d5 100644 --- a/google/genai/_gaos/utils/unions.py +++ b/google/genai/_gaos/utils/unions.py @@ -17,14 +17,14 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" -from typing import Any +from typing import Any, Mapping -from pydantic import BaseModel, TypeAdapter, ValidationError -from .serializers import construct_unvalidated +from pydantic import BaseModel, TypeAdapter, ValidationError, ValidationInfo def parse_open_union( v: Any, + info: ValidationInfo, *, disc_key: str, variants: dict[str, Any], @@ -35,28 +35,47 @@ def parse_open_union( """Parse an open discriminated union value with forward-compatibility. Known discriminator values are dispatched to their variant types. - Unknown discriminator values — or known discriminator values whose - payload fails variant validation (e.g. a partial variant emitted by a - newer server) — produce an instance of the fallback class, preserving - the raw payload for inspection. + + The Unknown fallback only applies when the validation context carries + ALLOW_UNKNOWN_UNION_VARIANTS, which the SDK sets when deserializing + server responses. There, unknown discriminator values — or known + discriminator values whose payload fails variant validation (e.g. a + partial variant emitted by a newer server) — produce an instance of the + fallback class, preserving the raw payload for inspection. Without the + flag (e.g. user-constructed request payloads), invalid values raise so + mistakes surface locally instead of being sent to the server. Non-dict values and dicts missing the discriminator deliberately raise instead of falling back, so pydantic can try sibling branches of an enclosing union (e.g. None in Optional[...]). """ + # pylint: disable=import-outside-toplevel + from .serializers import ALLOW_UNKNOWN_UNION_VARIANTS + if isinstance(v, BaseModel): return v if not isinstance(v, dict) or disc_key not in v: raise ValueError(f"{union_name}: expected object with '{disc_key}' field") + context = info.context + fallback_allowed = isinstance(context, Mapping) and bool( + context.get(ALLOW_UNKNOWN_UNION_VARIANTS) + ) disc = v[disc_key] variant_cls = variants.get(disc) - if variant_cls is not None: - try: - if isinstance(variant_cls, type) and issubclass(variant_cls, BaseModel): - return variant_cls.model_validate(v) - return TypeAdapter(variant_cls).validate_python(v) - except ValidationError: - if lenient: - return construct_unvalidated(v, variant_cls) + if variant_cls is None: + if fallback_allowed: return unknown_cls(raw=v) - return unknown_cls(raw=v) + raise ValueError(f"{union_name}: unrecognized {disc_key} value {disc!r}") + try: + if isinstance(variant_cls, type) and issubclass(variant_cls, BaseModel): + return variant_cls.model_validate(v, context=info.context) + return TypeAdapter(variant_cls).validate_python(v, context=info.context) + except ValidationError: + if not fallback_allowed: + raise + if lenient: + # pylint: disable=import-outside-toplevel + from .serializers import construct_unvalidated + + return construct_unvalidated(v, variant_cls) + return unknown_cls(raw=v)