STT provider seam with native Deepgram support (LC-593) #577

Merged
longjacksonle merged 1 commit from feat/lc593-stt-providers into main 2026-07-21 14:00:38 +02:00

Closes LC-593, the "provider flexibility" child of LC-589, on top of the LC-591 SttResult foundation you had me land first.

The problem

ReqwestSttClient was hardcoded to the OpenAI /v1/audio/transcriptions multipart wire shape and {text} response. A Deepgram/AssemblyAI-style API (different endpoint, auth header, JSON request/response) needed an external OpenAI-compatible shim in front.

The seam

SttProvider (openai default, deepgram) from LETS_CHAT_STT_PROVIDER; an unrecognized value falls back to openai rather than erroring. transcribe dispatches:

  • OpenAI: the existing multipart + verbose_json path, moved verbatim into transcribe_openai. Default and unchanged.
  • Deepgram: transcribe_deepgram posts the raw audio as the request body (not multipart), with Authorization: Token <key> (Deepgram's scheme, not Bearer) and model / language / punctuate / smart_format as query params. It parses results.channels[0].alternatives[0] - transcript for text, word timings aggregated into one clip-level segment so the LC-591 duration/VTT path consumes it with no downstream changes.

Both stay on outbound_trusted_post (the trusted, un-SSRF-filtered path). Deepgram has no prompt equivalent, so the operator glossary is documented as OpenAI-only.

Testability

The pieces that would otherwise need a live endpoint are split into pure functions - deepgram_query, parse_deepgram_result, plus the existing parse_openai_result - so the request shape and the response parse are unit-tested directly. That's the "request shape and response parse via a mock" the ticket asks for.

Tests

  • provider selection from env: deepgram selected, unknown value falls back to openai, case/whitespace tolerated
  • Deepgram query shape: model/punctuate/smart_format always; language only when hinted (trimmed)
  • Deepgram response parse: word timings aggregate to one segment with the real duration (1.5s in the fixture); no words -> empty segments (synthetic fallback); malformed body -> empty, not an error

The OpenAI path is unchanged (moved verbatim) and stays covered by its LC-591 unit tests and the transcripts integration suite. Full suite green (184 test binaries), just check clean.

Docs

LETS_CHAT_STT_PROVIDER in .env.standalone, .env.saas, and README.md, with the Deepgram URL/model/Token-auth specifics and the prompt-is-OpenAI-only caveat.

This is the last of the transcription-hardening children I have in flight; the remaining LC-589 siblings (LC-590 reliability, LC-592 cost/load) are separate and untouched here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5

Closes LC-593, the "provider flexibility" child of LC-589, on top of the LC-591 `SttResult` foundation you had me land first. ## The problem `ReqwestSttClient` was hardcoded to the OpenAI `/v1/audio/transcriptions` multipart wire shape and `{text}` response. A Deepgram/AssemblyAI-style API (different endpoint, auth header, JSON request/response) needed an external OpenAI-compatible shim in front. ## The seam `SttProvider` (`openai` default, `deepgram`) from `LETS_CHAT_STT_PROVIDER`; an unrecognized value falls back to `openai` rather than erroring. `transcribe` dispatches: - **OpenAI**: the existing multipart + `verbose_json` path, moved verbatim into `transcribe_openai`. Default and unchanged. - **Deepgram**: `transcribe_deepgram` posts the raw audio as the request body (not multipart), with `Authorization: Token <key>` (Deepgram's scheme, not Bearer) and `model` / `language` / `punctuate` / `smart_format` as query params. It parses `results.channels[0].alternatives[0]` - transcript for text, word timings aggregated into one clip-level segment so the LC-591 duration/VTT path consumes it with no downstream changes. Both stay on `outbound_trusted_post` (the trusted, un-SSRF-filtered path). Deepgram has no `prompt` equivalent, so the operator glossary is documented as OpenAI-only. ## Testability The pieces that would otherwise need a live endpoint are split into pure functions - `deepgram_query`, `parse_deepgram_result`, plus the existing `parse_openai_result` - so the request shape and the response parse are unit-tested directly. That's the "request shape and response parse via a mock" the ticket asks for. ## Tests - provider selection from env: `deepgram` selected, unknown value falls back to `openai`, case/whitespace tolerated - Deepgram query shape: `model`/`punctuate`/`smart_format` always; `language` only when hinted (trimmed) - Deepgram response parse: word timings aggregate to one segment with the real duration (1.5s in the fixture); no words -> empty segments (synthetic fallback); malformed body -> empty, not an error The **OpenAI path is unchanged** (moved verbatim) and stays covered by its LC-591 unit tests and the transcripts integration suite. Full suite green (184 test binaries), `just check` clean. ## Docs `LETS_CHAT_STT_PROVIDER` in `.env.standalone`, `.env.saas`, and `README.md`, with the Deepgram URL/model/`Token`-auth specifics and the prompt-is-OpenAI-only caveat. This is the last of the transcription-hardening children I have in flight; the remaining LC-589 siblings (LC-590 reliability, LC-592 cost/load) are separate and untouched here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
feat(stt): provider seam with native Deepgram support (LC-593)
All checks were successful
check-secrets / Nosey parker (push) Successful in 8s
check-secrets / TruffleHog (push) Successful in 9s
check-secrets / Kingfisher (push) Successful in 11s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 7s
check-secrets / TruffleHog (pull_request) Successful in 9s
Check / clippy + fmt + tests (pull_request) Successful in 13m28s
Create release / Create release from merged PR (pull_request) Has been skipped
f6207c3290
Closes the transcription "provider flexibility" child of LC-589. `ReqwestSttClient` was hardcoded to the OpenAI `/v1/audio/transcriptions` multipart shape, so Deepgram/AssemblyAI-style APIs (different endpoint, auth, and JSON) needed an external OpenAI-compatible shim. This lands a provider seam and Deepgram end to end, both normalizing onto the LC-591 `SttResult { text, segments }`.

`SttProvider` (`openai` default, `deepgram`) is read from `LETS_CHAT_STT_PROVIDER`; an unrecognized value falls back to `openai` rather than erroring. `transcribe` dispatches on it:
- OpenAI: the existing multipart + verbose_json path, moved verbatim into `transcribe_openai`. Default and unchanged.
- Deepgram: `transcribe_deepgram` posts the raw audio as the body (not multipart) with `Authorization: Token <key>` (Deepgram's scheme, not Bearer) and model/language/punctuate/smart_format as query params, then parses `results.channels[0].alternatives[0]` - transcript for text, word timings aggregated into one clip-level segment so the LC-591 duration/VTT path consumes it unchanged. Deepgram has no `prompt` equivalent, so the operator glossary is documented as OpenAI-only.

Both providers stay on `outbound_trusted_post` (the un-SSRF-filtered trusted path). The request builders and response parsers are split into pure functions (`deepgram_query`, `parse_deepgram_result`, the existing `parse_openai_result`) so the wire shape and parsing are unit-testable without a live endpoint.

Tests: provider selection from env (including the unknown-value fallback and the trim), the Deepgram query shape (model/punctuate/smart_format always, language only when hinted), Deepgram response parse with word timings -> real duration, no-words -> empty segments (synthetic fallback), and malformed-body -> empty not error. The OpenAI path is unchanged and stays covered by its LC-591 unit tests plus the transcripts integration suite. Full suite green (184 test binaries), `just check` clean.

Docs: `LETS_CHAT_STT_PROVIDER` in `.env.standalone`, `.env.saas`, and `README.md`, with the Deepgram URL/model/auth specifics and the prompt caveat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
longjacksonle deleted branch feat/lc593-stt-providers 2026-07-21 14:00:38 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!577
No description provided.