Auto-generated C# SDK for Smallest AI — Lightning text-to-speech and Pulse speech-to-text, plus voice cloning and pronunciation dictionaries. Covers both the REST API (OpenAPI) and both WebSocket channels (AsyncAPI).
dotnet build SmallestAI.slnx
dotnet test src/tests/IntegrationTests/Tests read SMALLESTAI_API_KEY, falling back to SMALLEST_API_KEY, and report inconclusive
(not failed) when neither is set.
Standard bearer:
using var client = new SmallestAIClient(apiKey);The WebSocket clients take the same key and set Authorization: bearer <key> themselves — AutoSDK
generates that natively from --security-scheme Http:Header:Bearer, so unlike Deepgram/Inworld this
SDK needs no hand-written auth hook.
src/libs/SmallestAI/generate.sh— regeneration (OpenAPI + handcrafted AsyncAPI)src/libs/SmallestAI/scripts/select-fern-openapi.py— picks the right document out of Smallest's Fern multi-API selector pagesrc/libs/SmallestAI/asyncapi.yaml— handcrafted; see belowsrc/libs/SmallestAI/Generated/— never editsrc/libs/SmallestAI/Extensions/SmallestAIClient.SpeechToTextClient.cs— MEAIISpeechToTextClientsrc/libs/SmallestAI/Extensions/SmallestAISpeechToTextPropertyNames.cs—AdditionalPropertieskeyssrc/libs/SmallestAI/Extensions/SmallestAIRealtimeClient.Convenience.cs— realtime helperssrc/tests/IntegrationTests/Tests.Audio.cs— builds the two-speaker fixture at test timesrc/tests/IntegrationTests/Examples/— example tests, and the single source fordocs/examples/
| Spec | Namespace | Client(s) | Purpose |
|---|---|---|---|
openapi.yaml (upstream, via Fern) |
SmallestAI |
SmallestAIClient |
REST: TTS, pre-recorded STT, voices, cloning, pronunciation dicts, Electron chat |
asyncapi.yaml (handcrafted) |
SmallestAI.Realtime |
SmallestAISttLiveRealtimeClient, SmallestAITtsLiveRealtimeClient |
WebSocket: Pulse realtime STT, Lightning streaming TTS |
Smallest publishes no AsyncAPI document. Both WebSocket endpoints exist only as prose inside the
OpenAPI operation descriptions, so the spec is maintained by hand from the docs (same precedent as
the Inworld SDK). Sources are listed in the file's info.description; the protocol was
cross-checked against pipecat's SmallestSTTService and smallest-inc/smallest-ai-vercel-provider.
Two details matter when editing it:
- Model the connection query in
bindings.ws.query. That is what makes AutoSDK emit a typedConnectAsyncwith one named parameter per query parameter instead of leaving consumers to build URIs by hand. - Do not model binary audio frames as messages. The generated client already exposes
SendAsync(ArraySegment<byte>, WebSocketMessageType, bool, CancellationToken);SendAudioAsyncinExtensions/wraps it.
Speaker labels are the headline Pulse feature and work in both modes.
- Pre-recorded:
SpeechToText.TranscribeAsync(..., diarize: true, wordTimestamps: true).TranscriptionResponse.WordscarriesSpeaker+SpeakerConfidence;UtterancescarriesSpeaker. - Realtime:
SmallestAISttLiveRealtimeClient.ConnectAsync(diarize: SttLiveDiarize.True, ...).
Constraints worth remembering:
- Only frames with
is_finalcarrywords[]. Never render speaker attribution from interim frames —SttTranscriptionEvent.HasSpeakerLabelsencodes this check. - Speaker ids are zero-indexed and stable within one response or session only.
pulse-prodoes not diarize and has no streaming worker —wss://.../stt/live?model=pulse-proreturns400before the upgrade. The realtimeSttLiveModelenum therefore has onlyPulse, and the MEAI adapter rejectspulse-proup front with an explicit message.
- Interface:
ISpeechToTextClient(bothGetTextAsyncandGetStreamingTextAsync). - Namespace conflict: yes. The generated
SmallestAI.ISpeechToTextClientsub-client interface collides by simple name withMicrosoft.Extensions.AI.ISpeechToTextClient. Inside theSmallestAInamespace, alias it:using Meai = Microsoft.Extensions.AI;. - Pulse knobs go in through
SpeechToTextOptions.AdditionalPropertiesand speaker-labelled results come back through the response'sAdditionalProperties, keyed bySmallestAISpeechToTextPropertyNames.RawRepresentationcarries the full typed payload. <NoWarn>$(NoWarn);MEAI001</NoWarn>is required in both the library and the tests project.
Pulse spells its boolean query parameters as the strings "true"/"false", so AutoSDK emits them
as two-member enums — alphabetically, False first. ToWireFlag<TEnum> therefore resolves via
Enum.Parse<TEnum>("True"/"False"). An ordinal mapping would silently invert every flag, including
diarize.
generate.sh applies these before generation:
--required-path /waves/v1/stt/in the Fern selector. Pulse STT arrived silently via an upstream spec refresh; without the guard a future reshuffle could pick a document without STT and delete the whole speech-to-text client with CI still green.- Audio-by-URL request variant on
/waves/v1/stt/. Upstream documentsContent-Type: application/jsonwith{"url": ...}in the operation description but declares onlyapplication/octet-stream, so the flow was unreachable. The patch yieldsTranscribeWithJsonAsync(including a conveniencestring urloverload).
The yq step also pretty-prints the spec, so auto-update diffs are reviewable rather than one
74 KB line.
CLI flags: --security-scheme Http:Header:Bearer, --generate-http-exception-hierarchy,
--generate-retry-handler, --ignore-openapi-errors, --exclude-deprecated-operations.
Spec sources:
- OpenAPI: selected from
https://docs.smallest.ai/openapi.json(a Fern multi-API selector page, not a spec — henceselect-fern-openapi.py) - AsyncAPI: handcrafted in-repo
- PackageId:
SmallestAI