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
----------