Skip to content

feat(voice): allow any OpenAI-compatible transcription endpoint - #22

Closed
pedroloch wants to merge 2 commits into
benedict2310:mainfrom
pedroloch:feat/configurable-transcription-endpoint
Closed

pedroloch wants to merge 2 commits into
benedict2310:mainfrom
pedroloch:feat/configurable-transcription-endpoint

Conversation

@pedroloch

Copy link
Copy Markdown

Problem

The cloud transcription backend hard-codes both the endpoint and the model name:

form.append("model", "whisper-1");
const response = await fetch("https://api.openai.com/v1/audio/transcriptions", { ... });

The request itself is plain multipart against an OpenAI-shaped /audio/transcriptions route, so nothing about it is OpenAI-specific except those two literals. Users who prefer another provider - for cost, for locale-specific accuracy, or because they already pay for one - have no way to use it short of patching dist/.

Change

Four optional environment variables. Each one defaults to the current behaviour, so an existing OPENAI_API_KEY install needs no change and behaves identically.

Variable Default
TELEPI_TRANSCRIPTION_API_KEY falls back to OPENAI_API_KEY
TELEPI_TRANSCRIPTION_URL https://api.openai.com/v1/audio/transcriptions
TELEPI_TRANSCRIPTION_MODEL whisper-1
TELEPI_TRANSCRIPTION_AUTH_HEADER unset, meaning Authorization: Bearer <key>

The auth header is configurable because not every OpenAI-compatible provider accepts the Bearer form; some document their own header instead. When the variable is unset, the header is built exactly as before.

Error messages now append [endpoint: <url>]. The existing message text is unchanged - this is only a suffix - because a 401 from a mistyped endpoint was previously indistinguishable from a 401 from a bad key.

Verification

npm test - 426/426 passing, including three new cases in test/voice.test.ts:

  • custom endpoint, model and auth header are all used
  • the Bearer form is kept when only endpoint and model are overridden
  • TELEPI_TRANSCRIPTION_API_KEY takes precedence over OPENAI_API_KEY

Also exercised end to end against a real third-party provider (SipPulse AI), with the local backends forced unavailable so the cloud path was the only one reachable:

backend usado: openai | ms: 6313
texto: Testando a transcricao ... Uma, duas, tres.

Both auth forms were confirmed to work against that provider - Authorization: Bearer and the provider's own api-key header - so the header variable is exercised, not speculative. Ogg Opus (what Telegram actually delivers) and MP3 both returned 200.

tsc --noEmit is clean.

Docs

README.md gets a row in the backend table and a subsection with the variable table plus a worked example; .env.example gets the four commented variables.

The cloud backend hard-coded both the endpoint and the model name, so the
only reachable provider was OpenAI itself even though the request is plain
multipart against an OpenAI-shaped /audio/transcriptions route.

Four optional variables, each defaulting to the previous behaviour, so an
existing OPENAI_API_KEY install needs no change:

  TELEPI_TRANSCRIPTION_API_KEY      falls back to OPENAI_API_KEY
  TELEPI_TRANSCRIPTION_URL          default https://api.openai.com/v1/audio/transcriptions
  TELEPI_TRANSCRIPTION_MODEL        default whisper-1
  TELEPI_TRANSCRIPTION_AUTH_HEADER  unset -> Authorization: Bearer <key>

The auth header is configurable because not every compatible provider
accepts the Bearer form; SipPulse AI, for one, documents an `api-key`
header. When the variable is unset nothing changes.

Error messages now carry the endpoint, which is the one piece of context
that was missing when a misconfigured URL returned a 401.
The OpenAI transcription API accepts a `prompt` field: free-form context
that biases decoding toward names and jargon the model would otherwise
miss. TELEPI_TRANSCRIPTION_PROMPT forwards it, and the field is omitted
entirely when the variable is unset, so providers that reject it are
unaffected.

Measured against a real provider, same clip, same model
(pulse-precision-pro), audio saying "Manda a Hipatia revisar o worktree
do Maestro":

  without prompt   Manda a Ipate a revisar o osc3 do maestro.
  with prompt      Manda a Hipatia revisar o worktree do Maestro.

Two things worth knowing, both found by testing rather than reading:

Support varies by MODEL, not only by provider. The same provider's
telephony model returned byte-identical output with and without the
field - it silently ignores it - while its precision model honoured it.
Nothing in either response says which happened.

Phrasing matters. The identical vocabulary as a bare comma list left the
name wrong; the same terms inside a context sentence fixed it. The README
says so rather than leaving it to be rediscovered.

@benedict2310 benedict2310 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the configuration design is backward-compatible and the tests cover custom URL/model/header/key precedence and prompt behavior well. I also ran the full branch locally: 427 tests pass and TypeScript builds cleanly.

Two issues should be fixed before merge:

  1. A rejected fetch() (malformed URL, DNS failure, refused connection) bypasses the new endpoint context entirely and surfaces only the native error. Please catch transport failures and wrap them with useful endpoint context.

  2. Endpoint context must be redacted before it reaches errors/Telegram. The current full URL suffix can expose query tokens, fragments, or URL credentials. Include only a safe representation (at minimum strip username/password, query, and fragment) for both HTTP failures and missing-text responses, and cover this with tests.

Minor docs/UX cleanup: README says “three transcription backends” but lists four, and custom providers still appear as backend openai; the latter can remain for compatibility if explicitly documented, but is misleading otherwise.

@benedict2310

Copy link
Copy Markdown
Owner

Thank you for this useful proposal. I rebuilt and merged its product-worthy core as #24 on current main, including the requested safe transport-error context, URL credential/query/fragment redaction (including URL text inside a transport error), focused tests, and corrected documentation. Closing this now-superseded branch to keep the PR queue clear.

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.

2 participants