Skip to content

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

Draft
luke-speechify wants to merge 5 commits into
mainfrom
sdk-release
Draft

feat!: regenerate SDK for API 2026-09-13 with consent-challenge voice cloning#16
luke-speechify wants to merge 5 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.
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