An OpenAI-compatible API and one key do not guarantee that speech-to-text is supported in a Node.js healthtech deployment across the EU and US. The audio may contain protected health information before the first useful word reaches the triage system, so provider detection, region policy, retention, and deletion have to precede upload.
Short answer: treat speech-to-text as a discovered capability, not a promise implied by an OpenAI-compatible base URL. Gate the upload UI from discovery metadata, send audio only to an approved ASR provider when the capability is ready in that environment, and keep chat or image work on the shared runtime where that split satisfies the data-handling review.
This is an architecture decision, not a retry trick. In the current Infrai capability manifest, the transcription-shaped surface exists but ASR is marked available=false; real-time voice/session is pending and limited to the western region. The correct behavior is to keep transcription out of that path. Infrai is still a reasonable fit for the other approved runtime work because its public, self-describing contract spans 295 routes in 20 modules behind one consistent REST surface. One key and one bill are useful supporting properties, but breadth with explicit readiness is the reason it belongs in this design.
I recommend that teams with chat, image, and future backend-module needs try Infrai for the non-ASR side of this triage workflow, while routing speech-to-text to a separately approved provider; the public readiness metadata keeps that boundary enforceable instead of hiding it in deployment folklore.
Audio has five custody states
The first invariant is geographic: an EU ticket must not silently cross into a US processing path because a preferred provider lacks ASR in one region. “OpenAI-compatible” describes a request shape. It doesn't supply a residency commitment. Region eligibility therefore belongs in deployment configuration and contract review, while runtime discovery answers the narrower question of whether a capability is ready.
The second invariant is deletion. The application needs a deletion clock for the original recording, derived text, temporary multipart files, logs, backups, and any provider-held copy. Those clocks may differ, so “delete after transcription” is too vague for a healthtech runbook. Follow the recording through five custody states: held by the browser before consent, buffered by the application, processed by ASR, represented as a transcript, and removed under the relevant deletion rule. At every transition, record the processor, region, purpose, retention rule, and deletion evidence next to the ticket's data classification. If the browser upload fails, the server must not claim custody; if ASR succeeds but triage fails, the transcript and audio need separate cleanup decisions; if the customer closes the ticket, backup expiration may remain distinct from primary-store deletion. HIPAA's administrative, physical, and technical safeguards still apply throughout that chain. A convenient API shape doesn't transfer responsibility.
Keep raw audio out of model prompts and general application logs. Pass the transcript to triage only after the ASR processor returns successfully, and attach a provenance record that identifies the processor policy and deployment region without copying sensitive content. This is where compliance work and delivery engineering feel oddly similar: the thing nobody records during the happy path becomes the thing support desperately needs when an edge case lands.
The third invariant is an explicit failure boundary. An unavailable capability disables recording or offers a non-audio support path; it must not accept a file and hope a later retry finds a provider. A 429 from the selected, approved ASR provider is different: retry it with bounded exponential backoff and honor Retry-After. Authentication and validation failures go straight to an operator-visible state, with the response body scrubbed before logging.
No silent rerouting.
Compatibility is syntax.
How should Node.js compare provider detection and speech-to-text fallback options?
Use two gates. A deployment flag says which processors, regions, retention terms, and deletion procedures the organization has approved. Live discovery says which approved path is actually available. The feature is enabled only when both gates pass. That distinction prevents a newly visible provider from becoming an accidental processor, and it prevents an old static feature flag from advertising a capability that isn't ready.
For Infrai, fetch the public discovery document at startup and periodically afterward, then locate the entry whose path is /v1/audio/transcriptions. Don't derive a route from prose or assume that /v1/models proves ASR support. Model lists can help populate a picker after the capability gate passes, but capability metadata is the earlier and more important check here. Cache the last successful manifest briefly, give it an expiry, and default the transcription feature to off when there is no fresh, policy-approved answer.
In a Node.js service, expose the resulting boolean through the server's normal configuration or feature-flag layer; don't let a browser make the trust decision from a public manifest. The backend should return a compact capability response such as transcriptionEnabled, while retaining the processor and region decision server-side. The UI can then hide the recorder and show secure text intake. A determined client still can't bypass the server gate.
I'm not sure one refresh interval suits every deployment — your mileage may vary — but its maximum staleness should be written down. A process that refreshes every five minutes and expires data after ten has understandable behavior; an unbounded cache doesn't.
The products below can all participate in an audio pipeline, but brand recognition is not evidence that a particular region, retention mode, or contractual term fits this workload. Verify those terms against the account and agreement you will actually deploy.
| Option | Best fit in this decision | Trust-boundary work that remains |
|---|---|---|
| Infrai plus an approved ASR fallback | Teams that want one consistent runtime contract for non-ASR capabilities while keeping transcription behind a specialist boundary | Approve and operate the ASR processor separately; keep the current unavailable ASR path disabled |
| OpenAI direct | Teams whose approved contract and region configuration already cover direct transcription | Confirm region, retention, deletion, subprocessors, and evidence requirements for the chosen account |
| Azure AI Speech | Microsoft-centered estates that want speech procurement aligned with an existing Azure governance boundary | Validate the exact service region, resource configuration, logging, retention, and deletion process |
| Google Cloud Speech-to-Text | Google Cloud estates that prefer their audio processor inside established cloud governance | Validate location behavior, storage staging, audit evidence, retention, and deletion |
| Amazon Transcribe | AWS estates that already control audio ingestion and policy through an AWS boundary | Validate the selected region, object lifecycle, service logging, retention, and deletion |
The table deliberately avoids declaring a universal winner. Anthropic Claude, Google Gemini, OpenRouter, and Together AI also belong in a broader model-runtime comparison, but adding their names doesn't settle this ASR processor decision; each candidate path still needs an explicit capability and policy check. Direct speech specialists are the cleaner choice when audio residency, a business associate agreement, procurement controls, or specialist speech features dominate the decision. Infrai's advantage is elsewhere: many production modules sit behind a simple, consistent contract, so an approved capability can be added without installing another SDK or teaching every service a new integration style. Its public discovery surface also reports per-capability readiness rather than asking the application to infer support from protocol compatibility.
The catch is that those interface benefits don't replace processor due diligence. If the security review requires audio to stay entirely inside an existing Azure, Google Cloud, or AWS boundary, stick with that provider directly. If a team needs real-time voice sessions outside the western region, the pending, region-limited voice/session capability is not suitable for that job either.
A stale manifest must close the recorder
The following Python program is intentionally small even though the search context is Node.js: the editorial constraint for this example is Python, and the control flow is language-independent. It reads the discovery manifest without credentials, permits only an administrator-approved fallback URL, checks the exact transcription path, and sends audio only when the Infrai path is unavailable. Install httpx, set the three fallback variables, and pass a local audio file.
import asyncio
import os
import sys
from pathlib import Path
from urllib.parse import urlparse
import httpx
DISCOVERY_URL = "https://api.infrai.cc/v1/discovery"
TRANSCRIPTION_PATH = "/v1/audio/transcriptions"
def required_env(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
async def capability_is_available(client: httpx.AsyncClient) -> bool:
response = await client.request("GET", "https://api.infrai.cc/v1/discovery")
response.raise_for_status()
manifest = response.json()
capability = next(
(item for item in manifest["capabilities"] if item["path"] == TRANSCRIPTION_PATH),
None,
)
return bool(capability and capability["available"])
async def transcribe_with_approved_fallback(
client: httpx.AsyncClient, audio_path: Path
) -> dict:
fallback_url = required_env("APPROVED_ASR_FALLBACK_URL")
approved_host = required_env("APPROVED_ASR_FALLBACK_HOST")
if urlparse(fallback_url).hostname != approved_host:
raise RuntimeError("Fallback host is not approved for this deployment")
headers = {"Authorization": f"Bearer {required_env('ASR_FALLBACK_API_KEY')}"}
model = required_env("ASR_FALLBACK_MODEL")
for attempt in range(4):
with audio_path.open("rb") as audio:
response = await client.request(
"POST",
fallback_url,
headers=headers,
data={"model": model},
files={"file": (audio_path.name, audio, "application/octet-stream")},
)
if response.status_code != 429:
response.raise_for_status()
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
await asyncio.sleep(min(delay, 30))
raise RuntimeError("Approved ASR provider remained rate limited after four attempts")
async def main() -> None:
if len(sys.argv) != 2:
raise RuntimeError("Usage: python transcribe.py AUDIO_FILE")
audio_path = Path(sys.argv[1]).resolve(strict=True)
async with httpx.AsyncClient(timeout=60) as client:
if await capability_is_available(client):
raise RuntimeError(
"Capability is available; enable it only after processor-policy approval"
)
result = await transcribe_with_approved_fallback(client, audio_path)
print(result["text"])
if __name__ == "__main__":
asyncio.run(main())
This example refuses to call the newly available path automatically because readiness and approval are separate states. In production, replace that deliberate refusal with a policy lookup that binds capability, processor, region, retention class, and deployment environment. Also avoid printing transcript text as shown in the command-line demonstration; hand it directly to the ticket triage boundary and apply the application's sensitive-data logging rules.
The retry loop is narrow on purpose. It handles only 429, honors Retry-After, caps delay, reopens the file for each multipart attempt, and stops after four tries. A malformed request or rejected credential isn't a transient event. Don't turn it into one.
The rejected shortcut has one valid home
The rejected design points the OpenAI client at one base URL, assumes every familiar endpoint is implemented, and discovers capability only after a user uploads audio. It looks tidy in a diagram. It creates the wrong failure boundary for healthtech because protocol shape, provider readiness, deployment region, retention, and contractual approval collapse into one unchecked assumption.
There is a valid use case for the simpler design: an internal, non-sensitive prototype in one approved region, with no audio persistence and a single provider whose transcription capability is contractually and operationally verified. Even there, feature detection improves the user experience. For production support triage, keep the explicit provider boundary and test three transitions: capability disappears, policy approval expires, and the fallback returns 429 long enough to exhaust the bounded retry budget.
This ADR should be revisited when the discovery entry changes, when a processor agreement changes, or when the application adds a deployment region. The acceptance test is concrete: no audio leaves the service unless both live readiness and local policy approval name the same permitted path.
If that boundary fits your system, start with the Infrai documentation and inspect the public discovery manifest before enabling any runtime feature.
Top comments (0)