Skip to content

feat!: regenerate SDK for API 2026-09-13 with consent-challenge voice cloning - #16

Merged
luke-speechify merged 22 commits into
mainfrom
sdk-release
Aug 19, 2026
Merged

luke-speechify merged 22 commits into
mainfrom
sdk-release

Conversation

@luke-speechify

@luke-speechify luke-speechify commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

SDK regeneration against API version 2026-09-13. Adds the models endpoint, the consent-challenge flow for voice cloning, and streaming with word-level timestamps. Also repairs the release-please wiring so a release bumps every version-bearing file.

Breaking changes

voices.create() signature

The consent: string parameter is gone. Voice cloning now goes through a two-step consent challenge, and the create carries a second audio file — the speaker reading the challenge phrase aloud.

// before
await client.voices.create({
  name: "my-voice",
  gender: "male",
  sample: fs.createReadStream("sample.wav"),
  consent: '{"fullName":"Jane Doe","email":"jane@example.com"}',
})

// after
const challenge = await client.voices.consentChallenges.create({ full_name: "Jane Doe" })
// challenge.phrase must be shown verbatim and read aloud into consent.wav
await client.voices.create({
  name: "my-voice",
  gender: "male",
  sample: fs.createReadStream("sample.wav"),
  consent_challenge_id: challenge.id,
  consent_recording: fs.createReadStream("consent.wav"),
})

A challenge is single-use and expires — see challenge.expires_at, which is the only authority on the window.

audio.stream() request shape

audio.stream() now takes StreamAudioRequest instead of GetStreamRequest. Accept stays a top-level header field; every body field moves under a new required body property.

GetStreamRequest has not been removed — it is now the body type (StreamAudioRequest["body"]) and has moved from resources/audio/client/requests/ to api/types/.

// before
await client.audio.stream({ input: "Hello", voice_id: "george", Accept: "audio/mpeg" })

// after
await client.audio.stream({ Accept: "audio/mpeg", body: { input: "Hello", voice_id: "george" } })

Default API version

Bumped from 2026-07-07 to 2026-09-13. Pass version explicitly to stay on the old behaviour.

New

  • client.models.list() — available TTS models
  • client.voices.consentChallenges.create() — consent challenge flow
  • client.audio.streamWithTimestamps() — SSE stream with word-level speech marks
  • voices.list() filters: type, locale, gender, model
  • ContentTooLargeError (413) and new error codes
  • core Stream class for JSON/SSE streaming

Release wiring

The generic release-please updater was silently no-opping — main currently ships 3.0.1 while reporting X-Fern-SDK-Version: 2.0.1. Fixed here:

  • Stopped relying on x-release-please-version markers. src/BaseClient.ts is Fern-generated and cannot be .fernignored — Fern legitimately owns it — so every regen strips the marker and the generic updater silently goes back to no-opping. Both generic extra-files entries and both marker comments are gone. src/version.ts leaves .fernignore too: it no longer needs shielding, so Fern owns it again.
  • The release tag is now the source of truth. The publish job stamps SDK_VERSION, X-Fern-SDK-Version, the User-Agent version, and package.json version from tag_name. The stamp fails when a target literal is missing, so a regen that renames or restructures those lines breaks the release loudly instead of shipping a stale version silently. Prerelease suffixes survive verbatim (4.0.0-alpha.1).
  • The stamp runs before the build. pnpm build emits dist/, and dist/ is what npm ships — a stamp placed after it would fix the sources and still publish the stale strings baked into the artifact. Order is stamp → build → assert → publish, and the assertion now covers dist/ in both CJS and ESM as well as the sources.
  • Added .fern/metadata.json to extra-files (it was stale at 2.0.1). It is jsonpath-based, so no marker is involved and a regen cannot disarm it.
  • Corrected repository.url casing to Speechify-AI; npm provenance 422s on a case-inexact URL.
  • Restored AGENTS.md and manual-publish.yml, both now in .fernignore.
  • Publish now fails if any version string — in source or in dist/ — or the repository URL disagrees with the tag.

The stamp rewrites the CI checkout only and is never committed back, so src/version.ts and src/BaseClient.ts read stale on main between releases by design; the published artifact is always built from the stamped and asserted tree. AGENTS.md documents this so it does not get "fixed" later.

Verified by executing the stamp and assertion scripts extracted from the workflow YAML against sandbox copies of the repo: matching tag, un-stamped tree at 4.0.0, assert-without-stamp control, three renamed/restructured literals, empty tag, non-semver tag, prerelease 4.0.0-alpha.1, lowercase repository.url, a dist/ built before the stamp, a missing dist/, and a repeat run for idempotency — 51 assertions, all passing. Running release-please's own updaters against the new config for 4.0.0 confirms package.json and .fern/metadata.json still update and that zero entries depend on a marker. pnpm build and pnpm test are clean (29 files, 523 tests).

Merge with squash so the conventional title and this footer land on main.

BREAKING CHANGE: voices.create() no longer accepts consent; it requires consent_challenge_id and consent_recording.
BREAKING CHANGE: audio.stream() takes StreamAudioRequest with body fields nested under body.
BREAKING CHANGE: default API version is now 2026-09-13.

Release-As: 4.0.0

@luke-speechify luke-speechify changed the title feat!: regenerate SDK for updated API specifications feat!: regenerate SDK for API 2026-09-13 with consent-challenge voice cloning Aug 14, 2026
@luke-speechify
luke-speechify marked this pull request as draft August 14, 2026 22:26
@luke-speechify

Copy link
Copy Markdown
Collaborator Author

Holding this as draft until the SSE response is modelled upstream.

Live verification against production found streamWithTimestamps() delivers its events but strips the event type. A wire capture showed 57 frames — 56 speech.chunk and 1 speech.done — and 0 of 57 data payloads carry a type field. The discriminator exists only on the SSE event: line.

audio/client/Client.ts:335-337 sets eventShape: { type: "sse" } but omits eventDiscriminator, so Stream.ts:72-76 routes to iterDataMessages() instead of iterSseEvents(), and Stream.ts:97-99 discards every line without a data: prefix. eventDiscriminator is set by no client in the SDK, so iterSseEvents() is unreachable. Consumers can't distinguish a chunk from the terminal event except by sniffing for billable_characters_count — which defeats the SDK's own instruction to ignore unrecognised event types.

Also found:

  • Stream<string> is a type-level lie: declared at Client.ts:290, cast with as any at :333, yields objects at runtime. The generated wire test bakes it in (data: "string"), so the suite never sees a real payload.
  • 413 is not mapped on any audio method — no case 413 in Client.ts:102-142, :214-254, :344-384. ContentTooLargeError exists but is wired only to the voices client.
  • SDK_VERSION ships with the correct value but is unreachable: never re-exported from src/index.ts, and the deep path is blocked by the exports map.

All of this is in generated code, so the fixes belong in the Fern definition. The release-please wiring in this PR is verified and stays as-is.

Everything else passed: 20/23 live cases, all 10 endpoints exercised, both new subpath exports resolve in ESM and CJS, tsc --noEmit clean under strict, all four voices.list filters proven to actually filter, voice cloning works with a synthesized consent recording.

Covers .fern/metadata.json, stamps the Fern-generated version strings from the
release tag at publish time, corrects repository.url casing for npm provenance,
restores AGENTS.md + manual-publish.yml, and gates publish on a version/tag
assertion.

src/BaseClient.ts is Fern-generated and cannot be .fernignore'd, so an
x-release-please-version marker does not survive there: every regeneration
strips it and the generic updater goes back to silently no-opping, leaving
X-Fern-SDK-Version and User-Agent reporting a version the SDK is not. That is
why main ships 3.0.1 while sending 2.0.1. src/version.ts was shielded in
.fernignore as a workaround for the same problem, but 3e8ea41 had already proven
regen wins. Both generic extra-files entries and both markers are gone, and
src/version.ts leaves .fernignore so Fern owns it again.

The publish job now rewrites those literals from the release tag, and the stamp
fails when a target literal is not found, so a regeneration that renames or
restructures those lines breaks the release loudly instead of shipping a stale
version silently. Order is stamp, build, assert, publish: the build emits dist/,
which is what npm ships, so a stamp after it would fix the sources and still
publish stale strings inside the artifact. The assertion covers dist/ in both
CJS and ESM as well as the sources, so a build that missed the stamp cannot pass.

The stamp applies to the CI checkout only and is never committed back, so those
strings read stale on main between releases by design; the published artifact is
always built from the stamped and asserted tree. AGENTS.md documents this so it
does not get "fixed" later.
@luke-speechify
luke-speechify marked this pull request as ready for review August 18, 2026 15:25
luke-speechify and others added 7 commits August 18, 2026 19:03
streamWithTimestamps constructed core.Stream with no eventDiscriminator,
routing SSE through iterDataMessages(). That parser drops the event: name,
cannot concatenate a multi-line data: field, and discards a trailing event
with no terminating blank line. The discarded trailing event is speech.done,
which carries billable_characters_count. Passing eventDiscriminator: "type"
routes to iterSseEvents() and fixes all three.

The live API sends a populated event: speech.chunk name and also duplicates
type in the payload. The generated wire fixture showing an empty event: does
not match the API.

core.Stream assigned a private AbortController that nothing read, and
registered an abort listener closing over this. The handler was therefore a
no-op, and a reused AbortSignal retained every Stream. Both are replaced with
throwIfAborted().

The abort unit test passed on its own break statement rather than on the
abort. It is rewritten so it fails when abort breaks.

README and reference streaming examples now narrow on item.type, handle
speech.error (yielded, never thrown, because the 200 is already committed)
and show the base64 decode.

.fernignore shields these until the Fern TypeScript generator emits the
discriminator. fern-python-sdk@5.14.20 already generates a correct
discriminated union on type from the same API definition;
fern-typescript-sdk@3.70.1 emits none.
The fern-api[bot] regeneration in commit 6898297 reverted the
version-bearing properties from 4.0.0 back to 3.0.2. This restores 4.0.0
in package.json, src/version.ts, .fern/metadata.json sdkVersion, and the
X-Fern-SDK-Version and User-Agent literals in src/BaseClient.ts.

This PR is a major release: it carries a "Release-As: 4.0.0" footer plus
three "BREAKING CHANGE:" footers in its body, which is what reaches main
on squash merge and drives the next version.

.release-please-manifest.json is deliberately left at 3.0.1. Setting it
to 4.0.0 would make release-please treat 4.0.0 as already released and
never cut the tag.

No files were added to .fernignore. package.json and .fern/metadata.json
must stay regenerable, and a future regen clobbering these strings again
is a known two-writer conflict between Fern and release-please, not
something to fix by shielding.
This reverts commit d267778.

The fern-api[bot] rollback to 3.0.2 in 6898297 was correct by design.
AGENTS.md is explicit that SDK_VERSION in src/version.ts and the
X-Fern-SDK-Version and User-Agent literals in src/BaseClient.ts are stamped
from the release tag in the CI checkout at publish time and never committed
back, so they are expected to read stale on the branch. Pinning them by hand
buys nothing and is called out as something not to do.

package.json version and .fern/metadata.json sdkVersion are owned by
release-please and set as part of the release PR. Both are addressed
structurally, by a known field and a jsonpath, so a regen cannot disarm them.

The Release-As: 4.0.0 footer in the PR body is what drives the next version.
POST /v1/audio/speech and streamWithTimestamps return 413 payload_too_large for oversized bodies, but the audio client surfaced it as a generic SpeechifyError. Wire the typed ContentTooLargeError on the audio client to match the voices client. Client.ts is .fernignore-shielded so this survives regeneration.
@luke-speechify
luke-speechify merged commit 23a8e2f into main Aug 19, 2026
4 checks passed
luke-speechify pushed a commit that referenced this pull request Aug 19, 2026
🤖 I have created a release *beep* *boop*
---


##
[4.0.0](3.0.1...4.0.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* default API version is now `2026-09-13`.

### Features

* regenerate SDK for API 2026-09-13 with consent-challenge voice cloning
([#16](#16))
([23a8e2f](23a8e2f))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant