diff --git a/.spec-sha b/.spec-sha index 52ea54b..7dd08d4 100644 --- a/.spec-sha +++ b/.spec-sha @@ -1 +1 @@ -28bf2af12a8765adb95381c7160cfc5b5e4ddd12 +6ace098c56527cf0a0e5018b67ec454412ee6926 diff --git a/src/onepin/.fern/metadata.json b/src/onepin/.fern/metadata.json index 4ded201..6f11267 100644 --- a/src/onepin/.fern/metadata.json +++ b/src/onepin/.fern/metadata.json @@ -30,7 +30,7 @@ } ] }, - "originGitCommit": "968747c21f0b427963288a82817fbe21a736230b", + "originGitCommit": "9e634eff3eb65ab32b4a77b6fee5e0d27aa126cf", "originGitCommitIsDirty": false, "invokedBy": "ci", "ciProvider": "github" diff --git a/src/onepin/core/client_wrapper.py b/src/onepin/core/client_wrapper.py index 9ade057..4f1ec9b 100644 --- a/src/onepin/core/client_wrapper.py +++ b/src/onepin/core/client_wrapper.py @@ -33,7 +33,7 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "onepin/0.11.1", + "User-Agent": "onepin/0.12.1", "X-Fern-Language": "Python", "X-Fern-Runtime": f"python/{platform.python_version()}", "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", diff --git a/src/onepin/core/http_client.py b/src/onepin/core/http_client.py index 124dce2..409d267 100644 --- a/src/onepin/core/http_client.py +++ b/src/onepin/core/http_client.py @@ -3,6 +3,7 @@ import asyncio import email.utils import re +import socket import time import typing from contextlib import asynccontextmanager, contextmanager @@ -23,6 +24,39 @@ JITTER_FACTOR = 0.2 # 20% random jitter +def get_keepalive_socket_options( + idle: int = 60, + intvl: int = 30, + cnt: int = 5, +) -> typing.List[typing.Tuple[int, int, int]]: + """ + Build TCP keepalive socket options for the current platform. + + Keepalive probes keep otherwise-idle connections alive so that long, + non-streaming requests survive idle-connection reaping by a firewall, + load balancer, or NAT. The available socket constants are OS-dependent, + so each option is guarded and only emitted when the platform defines it: + + - ``SO_KEEPALIVE`` is portable (Linux/macOS/Windows). + - The idle-before-first-probe knob is ``TCP_KEEPIDLE`` on Linux and modern + Windows, but ``TCP_KEEPALIVE`` on macOS. + - ``TCP_KEEPINTVL`` / ``TCP_KEEPCNT`` exist on Linux/macOS/modern Windows. + + Passing these tuples to ``httpx.HTTPTransport(socket_options=...)`` / + ``httpx.AsyncHTTPTransport(socket_options=...)`` applies them to every + connection the transport opens. + """ + opts: typing.List[typing.Tuple[int, int, int]] = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)] + idle_const = getattr(socket, "TCP_KEEPIDLE", None) or getattr(socket, "TCP_KEEPALIVE", None) + if idle_const: + opts.append((socket.IPPROTO_TCP, idle_const, idle)) + if hasattr(socket, "TCP_KEEPINTVL"): + opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl)) + if hasattr(socket, "TCP_KEEPCNT"): + opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt)) + return opts + + def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: """ This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. diff --git a/src/onepin/reference.md b/src/onepin/reference.md index b2c9a79..0271a82 100644 --- a/src/onepin/reference.md +++ b/src/onepin/reference.md @@ -1934,7 +1934,7 @@ client.templates.get(
-**template_id:** `str` +**template_id:** `str` — Case-sensitive 8-character base62 template identifier.
@@ -2026,7 +2026,7 @@ client.templates.delete_template(
-**template_id:** `str` +**template_id:** `str` — Case-sensitive 8-character base62 template identifier.
@@ -2120,7 +2120,7 @@ client.templates.update_template(
-**template_id:** `str` +**template_id:** `str` — Case-sensitive 8-character base62 template identifier.
@@ -2254,7 +2254,7 @@ client.templates.estimate_template(
-**template_id:** `str` +**template_id:** `str` — Case-sensitive 8-character base62 template identifier.
@@ -2352,7 +2352,7 @@ client.templates.clone(
-**template_id:** `str` +**template_id:** `str` — Case-sensitive 8-character base62 template identifier.
@@ -2451,7 +2451,7 @@ client.templates.favorite_template(
-**template_id:** `str` +**template_id:** `str` — Case-sensitive 8-character base62 template identifier.
@@ -2527,7 +2527,7 @@ client.templates.unfavorite_template(
-**template_id:** `str` +**template_id:** `str` — Case-sensitive 8-character base62 template identifier.
@@ -2567,10 +2567,11 @@ Every filter accepts repeat-key OR semantics: Filters combine across fields with AND; within a field, values OR. `language` matches a voice when any of its declared locales matches any -requested value. Platform voices with no declared locales (catalog gaps) -are treated as general-use and match every language filter. User-uploaded -/ cloned voices with no declared locales are excluded — that state means -"language unknown" pending the clone flow's language detection. +requested value. A voice with no declared locales matches NO `language` +filter — it must positively declare a locale to surface under it. This holds +for platform and user-uploaded voices alike: an unclassified platform voice +(catalog gap) is not treated as general-use, and a user-uploaded/cloned voice +with no locale stays "language unknown" pending clone-flow detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -2770,10 +2771,18 @@ instead of hardcoding option lists (mirrors `GET /dictionary/languages`). Each item is `{value, label, count}`: `value` is passed straight back to `GET /voices`; `label` is the display name for providers/models and `null` elsewhere (the FE owns language + enum labels); `count` is the number of -matching voices. For `languages`/`models`, `count` counts only voices that -explicitly declare the value — "general-use" platform voices (no declared -locales/models) that `GET /voices` matches against every language/model filter -are not counted, so a chip's count can be lower than the `GET /voices` result. +matching voices. A language surfaces as one chip keyed by its canonical +allowlist locale: every declared locale is folded onto the locale the +`GET /voices` filter would match it against (bare `ko` and regioned `ko-kr` +both count under `ko-kr`), so variants never split into duplicate chips for +the identical filter. Model counts include only voices that explicitly declare +the model. Language counts include voices that declare the exact regional locale +plus voices that declare its bare family (`en` contributes to every supported +`en-*` locale). A voice with no declared `supported_models` is "general use" — +`GET /voices` matches it against every model filter but no `models` chip counts +it, so a model chip's count can be lower than the `GET /voices?model=` result. +Languages have no such gap: no-locale voices are excluded from both the language +chips and `GET /voices?language=`, so language chip counts match the row counts. Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). @@ -6501,8 +6510,11 @@ client.workflows.patch_workflow( List confirmed uploads attached to a workflow. Returns only uploads that have been confirmed (fully transferred and -committed to the workflow). In-progress or abandoned uploads are excluded. -Each item includes a short-lived download URL for the uploaded file. +committed to the workflow), plus confirmed uploads the workflow definition's +Script Input nodes reference by id (e.g. a file attached in the assistant +chat, which stays bound to its assistant session). In-progress or abandoned +uploads are excluded. Each item includes a short-lived download URL for the +uploaded file. diff --git a/src/onepin/templates/client.py b/src/onepin/templates/client.py index 5e47f61..af544b9 100644 --- a/src/onepin/templates/client.py +++ b/src/onepin/templates/client.py @@ -197,6 +197,7 @@ def get( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -246,6 +247,7 @@ def delete_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -303,6 +305,7 @@ def update_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -379,6 +382,7 @@ def estimate_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -437,6 +441,7 @@ def clone( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -485,6 +490,7 @@ def favorite_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -520,6 +526,7 @@ def unfavorite_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -739,6 +746,7 @@ async def get( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -796,6 +804,7 @@ async def delete_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -861,6 +870,7 @@ async def update_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -945,6 +955,7 @@ async def estimate_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -1011,6 +1022,7 @@ async def clone( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -1067,6 +1079,7 @@ async def favorite_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1110,6 +1123,7 @@ async def unfavorite_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. request_options : typing.Optional[RequestOptions] Request-specific configuration. diff --git a/src/onepin/templates/raw_client.py b/src/onepin/templates/raw_client.py index 7fce3d8..d1b4eaa 100644 --- a/src/onepin/templates/raw_client.py +++ b/src/onepin/templates/raw_client.py @@ -244,6 +244,7 @@ def get( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -317,6 +318,7 @@ def delete_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -396,6 +398,7 @@ def update_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -498,6 +501,7 @@ def estimate_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -578,6 +582,7 @@ def clone( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -653,6 +658,7 @@ def favorite_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -709,6 +715,7 @@ def unfavorite_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -972,6 +979,7 @@ async def get( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -1045,6 +1053,7 @@ async def delete_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -1124,6 +1133,7 @@ async def update_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -1226,6 +1236,7 @@ async def estimate_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -1306,6 +1317,7 @@ async def clone( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. workspace_id : typing.Optional[str] @@ -1381,6 +1393,7 @@ async def favorite_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1437,6 +1450,7 @@ async def unfavorite_template( Parameters ---------- template_id : str + Case-sensitive 8-character base62 template identifier. request_options : typing.Optional[RequestOptions] Request-specific configuration. diff --git a/src/onepin/types/template_out.py b/src/onepin/types/template_out.py index 271e7e7..6e8b50f 100644 --- a/src/onepin/types/template_out.py +++ b/src/onepin/types/template_out.py @@ -22,7 +22,7 @@ class TemplateOut(UniversalBaseModel): id: str = pydantic.Field() """ - Unique template identifier. + Case-sensitive 8-character base62 template identifier. """ name: str = pydantic.Field() diff --git a/src/onepin/types/voice_facet_item.py b/src/onepin/types/voice_facet_item.py index dee2a2b..f0ed419 100644 --- a/src/onepin/types/voice_facet_item.py +++ b/src/onepin/types/voice_facet_item.py @@ -18,13 +18,18 @@ class VoiceFacetItem(UniversalBaseModel): number of voices matching ``value`` under the current request context (tab/workspace scope + every OTHER active filter — see ``VoiceFacetsOut``). - For the ``languages`` and ``models`` dimensions ``count`` reflects only voices - that *explicitly declare* ``value`` in their ``supported_languages`` / - ``supported_models`` array. It does NOT include "general-use" platform voices - (a catalog gap — platform voices with no declared locales/models) which - ``GET /voices`` matches against *every* ``language`` / ``model`` filter. So a - chip's ``count`` can be lower than the row count - ``GET /voices?language=`` / ``?model=`` actually returns. + For ``models``, ``count`` includes only voices that explicitly declare ``value`` + in ``supported_models``. For ``languages``, it includes voices that declare the + exact regional locale plus voices that declare its bare family (for example, + ``en`` contributes to every supported ``en-*`` locale). + + A voice with no declared ``supported_models`` is "general use" — ``GET /voices`` + matches it against every ``model`` filter, yet it is counted in no ``models`` + chip, so a ``models`` chip's ``count`` can be lower than the row count + ``GET /voices?model=`` actually returns. ``languages`` has no such gap: + a voice with no declared locales is excluded from both the language chips AND + ``GET /voices?language=``, so language chip counts match the filtered + row counts. """ value: str diff --git a/src/onepin/types/voice_facets_out.py b/src/onepin/types/voice_facets_out.py index f15d356..5d6ddae 100644 --- a/src/onepin/types/voice_facets_out.py +++ b/src/onepin/types/voice_facets_out.py @@ -13,12 +13,13 @@ class VoiceFacetsOut(UniversalBaseModel): Two families of dimension: - * **Data-driven** — ``providers``, ``models``, ``languages``: only values - actually present in the scoped voices are returned (count is always ≥ 1; - count-0 values are omitted). Every value is guaranteed to be a valid - ``GET /voices`` filter (provider/model restricted to the enabled catalog, - language to the supported-locale allowlist), so selecting one never yields - a 422 or empty page. Sorted count DESC, then value ASC. + * **Data-driven** — ``providers``, ``models``, ``languages``: only values with + a scoped voice count are returned (count is always ≥ 1; count-0 values are + omitted). A language value may be derived from a bare family tag on a scoped + voice. Every value is guaranteed to be a valid ``GET /voices`` filter + (provider/model restricted to the enabled catalog, language to the + supported-locale allowlist), so selecting one never yields a 422 or empty + page. Sorted count DESC, then value ASC. * **Enum** — ``genders``, ``ages``, ``categories``, ``accents``: the FULL fixed enum is always returned in natural enum order, including count-0 values (the FE greys those out). ``label`` is ``None`` (the FE owns enum @@ -32,7 +33,11 @@ class VoiceFacetsOut(UniversalBaseModel): providers: typing.List[VoiceFacetItem] models: typing.List[VoiceFacetItem] - languages: typing.List[VoiceFacetItem] + languages: typing.List[VoiceFacetItem] = pydantic.Field() + """ + Region-qualified lowercase BCP-47 language filter options. Bare language values are never emitted; a bare family tag on a voice contributes to every supported regional sibling's count. + """ + genders: typing.List[VoiceFacetItem] ages: typing.List[VoiceFacetItem] categories: typing.List[VoiceFacetItem] diff --git a/src/onepin/types/voice_out.py b/src/onepin/types/voice_out.py index 5f4409c..c4e7610 100644 --- a/src/onepin/types/voice_out.py +++ b/src/onepin/types/voice_out.py @@ -105,7 +105,7 @@ class VoiceOut(UniversalBaseModel): supported_languages: typing.Optional[typing.List[str]] = pydantic.Field(default=None) """ - BCP-47 language codes this voice supports. Null for platform voices means the voice is treated as general-use across all locales. + BCP-47 language codes this voice supports. Null means the voice declares no locales; it is not matched by any `language` filter — a voice must positively declare a locale to surface under that filter. """ supported_models: typing.Optional[typing.List[str]] = pydantic.Field(default=None) diff --git a/src/onepin/types/voice_similar_out.py b/src/onepin/types/voice_similar_out.py index ee5c80f..d20d6fe 100644 --- a/src/onepin/types/voice_similar_out.py +++ b/src/onepin/types/voice_similar_out.py @@ -105,7 +105,7 @@ class VoiceSimilarOut(UniversalBaseModel): supported_languages: typing.Optional[typing.List[str]] = pydantic.Field(default=None) """ - BCP-47 language codes this voice supports. Null for platform voices means the voice is treated as general-use across all locales. + BCP-47 language codes this voice supports. Null means the voice declares no locales; it is not matched by any `language` filter — a voice must positively declare a locale to surface under that filter. """ supported_models: typing.Optional[typing.List[str]] = pydantic.Field(default=None) diff --git a/src/onepin/types/workflow_run_outputs_out.py b/src/onepin/types/workflow_run_outputs_out.py index 38ac3f1..73c4dcb 100644 --- a/src/onepin/types/workflow_run_outputs_out.py +++ b/src/onepin/types/workflow_run_outputs_out.py @@ -10,6 +10,7 @@ class WorkflowRunOutputsOut(UniversalBaseModel): run_id: str run_status: str + credits: typing.Optional[int] = None outputs: typing.List[WorkflowRunSinkOutputOut] if IS_PYDANTIC_V2: diff --git a/src/onepin/voices/client.py b/src/onepin/voices/client.py index 36cfe30..8ad7f77 100644 --- a/src/onepin/voices/client.py +++ b/src/onepin/voices/client.py @@ -66,10 +66,11 @@ def list( Filters combine across fields with AND; within a field, values OR. `language` matches a voice when any of its declared locales matches any - requested value. Platform voices with no declared locales (catalog gaps) - are treated as general-use and match every language filter. User-uploaded - / cloned voices with no declared locales are excluded — that state means - "language unknown" pending the clone flow's language detection. + requested value. A voice with no declared locales matches NO `language` + filter — it must positively declare a locale to surface under it. This holds + for platform and user-uploaded voices alike: an unclassified platform voice + (catalog gap) is not treated as general-use, and a user-uploaded/cloned voice + with no locale stays "language unknown" pending clone-flow detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -188,10 +189,18 @@ def get_voice_facets( item is `{value, label, count}`: `value` is passed straight back to `GET /voices`; `label` is the display name for providers/models and `null` elsewhere (the FE owns language + enum labels); `count` is the number of - matching voices. For `languages`/`models`, `count` counts only voices that - explicitly declare the value — "general-use" platform voices (no declared - locales/models) that `GET /voices` matches against every language/model filter - are not counted, so a chip's count can be lower than the `GET /voices` result. + matching voices. A language surfaces as one chip keyed by its canonical + allowlist locale: every declared locale is folded onto the locale the + `GET /voices` filter would match it against (bare `ko` and regioned `ko-kr` + both count under `ko-kr`), so variants never split into duplicate chips for + the identical filter. Model counts include only voices that explicitly declare + the model. Language counts include voices that declare the exact regional locale + plus voices that declare its bare family (`en` contributes to every supported + `en-*` locale). A voice with no declared `supported_models` is "general use" — + `GET /voices` matches it against every model filter but no `models` chip counts + it, so a model chip's count can be lower than the `GET /voices?model=` result. + Languages have no such gap: no-locale voices are excluded from both the language + chips and `GET /voices?language=`, so language chip counts match the row counts. Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). @@ -506,10 +515,11 @@ async def list( Filters combine across fields with AND; within a field, values OR. `language` matches a voice when any of its declared locales matches any - requested value. Platform voices with no declared locales (catalog gaps) - are treated as general-use and match every language filter. User-uploaded - / cloned voices with no declared locales are excluded — that state means - "language unknown" pending the clone flow's language detection. + requested value. A voice with no declared locales matches NO `language` + filter — it must positively declare a locale to surface under it. This holds + for platform and user-uploaded voices alike: an unclassified platform voice + (catalog gap) is not treated as general-use, and a user-uploaded/cloned voice + with no locale stays "language unknown" pending clone-flow detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -636,10 +646,18 @@ async def get_voice_facets( item is `{value, label, count}`: `value` is passed straight back to `GET /voices`; `label` is the display name for providers/models and `null` elsewhere (the FE owns language + enum labels); `count` is the number of - matching voices. For `languages`/`models`, `count` counts only voices that - explicitly declare the value — "general-use" platform voices (no declared - locales/models) that `GET /voices` matches against every language/model filter - are not counted, so a chip's count can be lower than the `GET /voices` result. + matching voices. A language surfaces as one chip keyed by its canonical + allowlist locale: every declared locale is folded onto the locale the + `GET /voices` filter would match it against (bare `ko` and regioned `ko-kr` + both count under `ko-kr`), so variants never split into duplicate chips for + the identical filter. Model counts include only voices that explicitly declare + the model. Language counts include voices that declare the exact regional locale + plus voices that declare its bare family (`en` contributes to every supported + `en-*` locale). A voice with no declared `supported_models` is "general use" — + `GET /voices` matches it against every model filter but no `models` chip counts + it, so a model chip's count can be lower than the `GET /voices?model=` result. + Languages have no such gap: no-locale voices are excluded from both the language + chips and `GET /voices?language=`, so language chip counts match the row counts. Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). diff --git a/src/onepin/voices/raw_client.py b/src/onepin/voices/raw_client.py index ce39276..ecfef41 100644 --- a/src/onepin/voices/raw_client.py +++ b/src/onepin/voices/raw_client.py @@ -62,10 +62,11 @@ def list( Filters combine across fields with AND; within a field, values OR. `language` matches a voice when any of its declared locales matches any - requested value. Platform voices with no declared locales (catalog gaps) - are treated as general-use and match every language filter. User-uploaded - / cloned voices with no declared locales are excluded — that state means - "language unknown" pending the clone flow's language detection. + requested value. A voice with no declared locales matches NO `language` + filter — it must positively declare a locale to surface under it. This holds + for platform and user-uploaded voices alike: an unclassified platform voice + (catalog gap) is not treated as general-use, and a user-uploaded/cloned voice + with no locale stays "language unknown" pending clone-flow detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -209,10 +210,18 @@ def get_voice_facets( item is `{value, label, count}`: `value` is passed straight back to `GET /voices`; `label` is the display name for providers/models and `null` elsewhere (the FE owns language + enum labels); `count` is the number of - matching voices. For `languages`/`models`, `count` counts only voices that - explicitly declare the value — "general-use" platform voices (no declared - locales/models) that `GET /voices` matches against every language/model filter - are not counted, so a chip's count can be lower than the `GET /voices` result. + matching voices. A language surfaces as one chip keyed by its canonical + allowlist locale: every declared locale is folded onto the locale the + `GET /voices` filter would match it against (bare `ko` and regioned `ko-kr` + both count under `ko-kr`), so variants never split into duplicate chips for + the identical filter. Model counts include only voices that explicitly declare + the model. Language counts include voices that declare the exact regional locale + plus voices that declare its bare family (`en` contributes to every supported + `en-*` locale). A voice with no declared `supported_models` is "general use" — + `GET /voices` matches it against every model filter but no `models` chip counts + it, so a model chip's count can be lower than the `GET /voices?model=` result. + Languages have no such gap: no-locale voices are excluded from both the language + chips and `GET /voices?language=`, so language chip counts match the row counts. Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). @@ -635,10 +644,11 @@ async def list( Filters combine across fields with AND; within a field, values OR. `language` matches a voice when any of its declared locales matches any - requested value. Platform voices with no declared locales (catalog gaps) - are treated as general-use and match every language filter. User-uploaded - / cloned voices with no declared locales are excluded — that state means - "language unknown" pending the clone flow's language detection. + requested value. A voice with no declared locales matches NO `language` + filter — it must positively declare a locale to surface under it. This holds + for platform and user-uploaded voices alike: an unclassified platform voice + (catalog gap) is not treated as general-use, and a user-uploaded/cloned voice + with no locale stays "language unknown" pending clone-flow detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -782,10 +792,18 @@ async def get_voice_facets( item is `{value, label, count}`: `value` is passed straight back to `GET /voices`; `label` is the display name for providers/models and `null` elsewhere (the FE owns language + enum labels); `count` is the number of - matching voices. For `languages`/`models`, `count` counts only voices that - explicitly declare the value — "general-use" platform voices (no declared - locales/models) that `GET /voices` matches against every language/model filter - are not counted, so a chip's count can be lower than the `GET /voices` result. + matching voices. A language surfaces as one chip keyed by its canonical + allowlist locale: every declared locale is folded onto the locale the + `GET /voices` filter would match it against (bare `ko` and regioned `ko-kr` + both count under `ko-kr`), so variants never split into duplicate chips for + the identical filter. Model counts include only voices that explicitly declare + the model. Language counts include voices that declare the exact regional locale + plus voices that declare its bare family (`en` contributes to every supported + `en-*` locale). A voice with no declared `supported_models` is "general use" — + `GET /voices` matches it against every model filter but no `models` chip counts + it, so a model chip's count can be lower than the `GET /voices?model=` result. + Languages have no such gap: no-locale voices are excluded from both the language + chips and `GET /voices?language=`, so language chip counts match the row counts. Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). diff --git a/src/onepin/workflows/client.py b/src/onepin/workflows/client.py index 6343c95..c5ed0cd 100644 --- a/src/onepin/workflows/client.py +++ b/src/onepin/workflows/client.py @@ -512,8 +512,11 @@ def list_workflow_uploads( List confirmed uploads attached to a workflow. Returns only uploads that have been confirmed (fully transferred and - committed to the workflow). In-progress or abandoned uploads are excluded. - Each item includes a short-lived download URL for the uploaded file. + committed to the workflow), plus confirmed uploads the workflow definition's + Script Input nodes reference by id (e.g. a file attached in the assistant + chat, which stays bound to its assistant session). In-progress or abandoned + uploads are excluded. Each item includes a short-lived download URL for the + uploaded file. Parameters ---------- @@ -1784,8 +1787,11 @@ async def list_workflow_uploads( List confirmed uploads attached to a workflow. Returns only uploads that have been confirmed (fully transferred and - committed to the workflow). In-progress or abandoned uploads are excluded. - Each item includes a short-lived download URL for the uploaded file. + committed to the workflow), plus confirmed uploads the workflow definition's + Script Input nodes reference by id (e.g. a file attached in the assistant + chat, which stays bound to its assistant session). In-progress or abandoned + uploads are excluded. Each item includes a short-lived download URL for the + uploaded file. Parameters ---------- diff --git a/src/onepin/workflows/raw_client.py b/src/onepin/workflows/raw_client.py index 376fade..e077453 100644 --- a/src/onepin/workflows/raw_client.py +++ b/src/onepin/workflows/raw_client.py @@ -680,8 +680,11 @@ def list_workflow_uploads( List confirmed uploads attached to a workflow. Returns only uploads that have been confirmed (fully transferred and - committed to the workflow). In-progress or abandoned uploads are excluded. - Each item includes a short-lived download URL for the uploaded file. + committed to the workflow), plus confirmed uploads the workflow definition's + Script Input nodes reference by id (e.g. a file attached in the assistant + chat, which stays bound to its assistant session). In-progress or abandoned + uploads are excluded. Each item includes a short-lived download URL for the + uploaded file. Parameters ---------- @@ -2387,8 +2390,11 @@ async def list_workflow_uploads( List confirmed uploads attached to a workflow. Returns only uploads that have been confirmed (fully transferred and - committed to the workflow). In-progress or abandoned uploads are excluded. - Each item includes a short-lived download URL for the uploaded file. + committed to the workflow), plus confirmed uploads the workflow definition's + Script Input nodes reference by id (e.g. a file attached in the assistant + chat, which stays bound to its assistant session). In-progress or abandoned + uploads are excluded. Each item includes a short-lived download URL for the + uploaded file. Parameters ----------