refactor: reorganize adapters into provider folders - #19
Conversation
Align every provider on the same adapter/helpers/mappers/exports layout so future refactors can stay consistent while keeping compatibility shims for existing imports. Made-with: Cursor
Remove dead duplicate implementations from the leftover adapter and mapper shims so the provider-folder sources stay the only runtime implementation. Made-with: Cursor
Drop the unused helpers and mapper compatibility layer plus empty placeholder modules so provider folders expose only real entrypoints. Made-with: Cursor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis pull request refactors all transcription provider adapters from monolithic single-file implementations into modular directory-based structures, each containing separated adapter class, exports, helpers, mappers, and barrel index. Additionally, new centralized modules for provider endpoints and shared types are introduced, and the main adapter index is updated to reflect new module paths. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 27
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/adapters/assemblyai/helpers.ts`:
- Around line 189-207: The condition checking chunk size is redundant: replace
the current "audioBuffer.length >= MIN_CHUNK_SIZE || audioBuffer.length >=
MAX_CHUNK_SIZE" with a single check against MIN_CHUNK_SIZE (i.e.,
"audioBuffer.length >= MIN_CHUNK_SIZE"); if you intended MAX_CHUNK_SIZE to force
an immediate flush, instead handle it explicitly by checking "audioBuffer.length
>= MAX_CHUNK_SIZE" first and then fall back to "audioBuffer.length >=
MIN_CHUNK_SIZE". Update the check around audioBuffer (used with
callbacks.onRawMessage and ws.send) accordingly.
In `@src/adapters/assemblyai/mappers.ts`:
- Around line 244-248: The forEach callbacks in the keyterms and keytermsPrompt
handling (variables keyterms and aaiOpts.keytermsPrompt, and calls to
params.append) produce implicit returns flagged by Biome; replace those forEach
usages with explicit loops (e.g., for...of) or convert the arrow callbacks to
block bodies that call params.append without returning a value so there is no
implicit return from the callback. Locate the code around keyterms =
options?.customVocabulary || aaiOpts.keyterms and the
aaiOpts.keytermsPrompt.forEach(...) and change them accordingly to avoid
implicit return values.
In `@src/adapters/azure-stt/adapter.ts`:
- Around line 166-167: The axios.get used when downloading resultFile content in
adapter.ts bypasses the adapter's configured timeout and can hang
getTranscript() and pollForCompletion(); update the download call to use the
adapter's configured timeout (or the adapter's axios/http client that already
has timeout configured) when fetching resultFile.links.contentUrl so that the
request honors the adapter timeout settings before calling
mapFromAzureResponse(transcription, ..., this.name).
In `@src/adapters/azure-stt/mappers.ts`:
- Around line 16-25: Replace this provider-specific copier normalizeAzureStatus
with the shared provider-aware normalizer: remove or stop exporting
normalizeAzureStatus and instead import and call the existing shared normalizer
(the provider-aware status normalizer used elsewhere) so Azure substring matches
are handled centrally; update the call sites to use that shared function and
ensure you do not reintroduce the old fallback of returning "queued" for unknown
statuses (let the shared normalizer’s fallback behavior remain in effect or
change it there if needed).
In `@src/adapters/deepgram/adapter.ts`:
- Around line 200-203: The hasMore pagination check is inverted; replace the
expression using data.page/data.requests with a check comparing the number of
returned items to the page limit: compute the effective limit (const limit =
data.limit || 10) and set hasMore based on whether transcripts.length equals
that limit (e.g., hasMore = transcripts.length === limit) so when a full page of
results is returned we mark there may be more; update the return that contains
transcripts and hasMore accordingly (refer to transcripts, hasMore, data.limit,
and data.requests?.length in adapter.ts).
In `@src/adapters/deepgram/mappers.ts`:
- Around line 301-325: The forEach callbacks in the Deepgram mapper (uses dgOpts
and params) use single-expression arrow functions which trigger lint warnings;
update each to use a block body with explicit braces and no return (e.g., change
dgOpts.customTopic.forEach((t) => params.append("custom_topic", t)) to
dgOpts.customTopic.forEach((t) => { params.append("custom_topic", t); });) —
apply the same pattern for dgOpts.customIntent.forEach, list.forEach (keywords),
dgOpts.keyterm.forEach, dgOpts.redact.forEach, and dgOpts.tag.forEach so all
callbacks use { ... } blocks and end statements with semicolons.
In `@src/adapters/elevenlabs/helpers.ts`:
- Around line 338-343: The close() implementation currently returns as soon as
ws.close() is called, causing races; change close (the close: async () => { ...
} function) to await the socket actually closing by returning a Promise that
sends the end_of_stream and calls ws.close(), then listens for the WebSocket
'close' and 'error' events (or checks ws.readyState === CLOSED) and only
resolves when the 'close' event fires (or rejects on error/timeout); update
status from "closing" to "closed" when the close event occurs and remove
listeners to avoid leaks.
In `@src/adapters/elevenlabs/index.ts`:
- Line 1: Add a new exports module to expose the generated ElevenLabs API
surface: create src/adapters/elevenlabs/exports.ts that re-exports the generated
files (e.g., from elevenLabsSpeechToTextAPI.ts and related schema types) so
consumers can import them, then update src/adapters/elevenlabs/index.ts (which
currently only exports "./adapter") to also export everything from "./exports";
ensure the exports file re-exports the specific generated symbols and types used
elsewhere so the public API mirrors other providers.
In `@src/adapters/elevenlabs/mappers.ts`:
- Around line 44-51: The speaker property assignment is using a redundant
nullish coalescing (speakerId ?? undefined) since speakerId is already derived
as w.speaker_id ?? undefined; update the Word construction (in the mapper where
const speakerId = w.speaker_id ?? undefined and the Word object is created) to
assign speaker: speakerId directly to remove the unnecessary ?? undefined.
In `@src/adapters/gladia/mappers.ts`:
- Around line 63-70: The current assignment to request.language_config
overwrites any provider-specific fields from options.gladia.language_config;
instead, merge them: start from options.gladia.language_config (or
request.language_config) and spread in options.codeSwitchingConfig, then set
languages to [options.language] if options.language is provided else preserve
existing languages, and set code_switching to options.codeSwitching if provided
else preserve existing code_switching; update the block that assigns
request.language_config (referencing request.language_config,
options.codeSwitchingConfig, options.language, options.codeSwitching, and
options.gladia.language_config) to perform this merge so provider-specific
fields are retained.
In `@src/adapters/openai-whisper/adapter.ts`:
- Around line 76-83: The code unsafely casts response.data to any when calling
mapFromOpenAIResponse; instead determine or define the correct response type
from createTranscription (or the underlying OpenAI API type) and update the
signature of mapFromOpenAIResponse to accept that concrete type (e.g.,
TranscriptionResponse). Change the call in the createTranscription handling to
pass response.data typed as that concrete type (not any), and if necessary
update CreateTranscriptionRequestModel usage and any parsing of
isDiarization/this.name to accommodate the real response shape so TypeScript
type safety is preserved.
In `@src/adapters/openai-whisper/helpers.ts`:
- Around line 75-88: The current handler for
REALTIME_SERVER_EVENTS.ConversationItemInputAudioTranscriptionCompleted emits
callbacks.onUtterance with placeholder timing (start: 0, end: 0, words: []);
update the case in the switch handling
ConversationItemInputAudioTranscriptionCompleted so you do not send misleading
zero timings—instead either (A) compute approximate start/end by using a tracked
cumulativeAudioDuration (e.g., a module/state variable you increment as audio
segments complete) and set words only if timing data exists, or (B) skip calling
callbacks.onUtterance entirely when no timing/word-level info is available; keep
callbacks.onTranscript as-is and ensure you reference transcription.transcript
and callbacks.onUtterance in the updated logic.
In `@src/adapters/openai-whisper/mappers.ts`:
- Around line 41-53: The code redundantly checks options?.diarization after
isDiarization has already been handled; update the logic in the block that sets
request.response_format (referencing isDiarization, options?.diarization,
request.response_format and OpenAIResponseFormat.verbose_json) to remove the
unnecessary options?.diarization branch or document the edge-case explicitly:
either drop the options?.diarization condition so only isDiarization (and
needsWords) determine verbose_json, or keep it but add a clarifying comment
explaining that options.diarization can override model-derived diarization when
a non-diarization model is used.
- Line 68: The current requestId assignment (const requestId =
`openai-${Date.now()}`) can collide when multiple events occur in the same
millisecond; update the code to produce a truly unique id by appending a random
suffix or using a UUID generator (e.g., replace with a call to a new helper like
generateRequestId()) and use that helper wherever requestId is created in this
module (reference: the requestId variable in mappers.ts and add a
generateRequestId function to return something like
`openai-${Date.now()}-${randomOrUUID}`).
- Around line 67-83: The current fragile detection in the mapper uses
Object.keys(response).length === 1 to identify a simple { text } response;
change this to an explicit shape check in the mapper function so you detect a
simple transcription by verifying that response.text exists and is a string
(e.g., typeof response.text === "string") and NOT relying on object key count,
or alternatively ensure absence of other known richer fields (like segments,
language, etc.) before mapping to the simple response shape; update the branch
that builds the simple result (the block that creates requestId, returns
success/provider/data with text/status/language/confidence, extended, tracking,
raw) to run when the explicit check passes so future added metadata (usage,
request_id) won’t break the logic.
In `@src/adapters/provider-endpoints.ts`:
- Around line 167-171: The Speechmatics case currently returns region endpoints
via SPEECHMATICS_REGION_ENDPOINTS[r] without applying any overrides for
WebSocket URLs; update the "speechmatics" branch (the case handling
"speechmatics", variables r and ep) to merge in overrides?.wsBaseUrl the same
way other providers do—e.g., return { ...ep, wsBaseUrl: overrides?.wsBaseUrl ??
ep.wsBaseUrl }—so config overrides for wsBaseUrl are honored.
- Around line 192-194: The default switch case currently returns an empty `{
api: "" }`, which masks unhandled TranscriptionProvider values and can cause
silent failures; update the default branch in this provider-endpoints switch to
fail loudly by throwing a descriptive Error (including the provider value) or,
if graceful degradation is required, log a warning and return a clear sentinel
(e.g., null or an explicit { api: undefined }) so misconfigurations surface;
reference the TranscriptionProvider enum and the switch's default branch that
currently returns `{ api: "" }` when making this change.
- Around line 172-179: In the soniox case the local variable r is set to (region
as "us" | "eu" | "jp") || "us", so the subsequent nullish fallback
SONIOX_REGION_ENDPOINTS[r] ?? SONIOX_REGION_ENDPOINTS.us is redundant; simplify
by directly indexing SONIOX_REGION_ENDPOINTS with r (or remove the "|| 'us'" and
keep a single fallback) and return {...ep, websocket: overrides?.wsBaseUrl ??
ep.websocket} as before—update the case "soniox" block to eliminate the
unreachable fallback and use only one clear default path for selecting the
endpoint (referencing SONIOX_REGION_ENDPOINTS, r, ep, and overrides?.wsBaseUrl).
In `@src/adapters/soniox/adapter.ts`:
- Around line 149-159: The getModels method currently catches errors and calls
console.error; remove the console.error usage and propagate the error instead so
callers can handle it: in async getModels (which calls validateConfig and
this.client!.get("/models")) replace the catch block to rethrow the caught error
(or throw a new Error that wraps the original with context like "Failed to fetch
Soniox models") rather than logging; if you prefer structured logging, add an
optional logger parameter or a logger on the adapter instance and use that
instead of console.error.
In `@src/adapters/soniox/helpers.ts`:
- Around line 254-259: The ws "error" handler currently swallows the error;
change the listener to accept the error parameter (e.g., ws.on("error", (err) =>
...)) and pass the real error info into callbacks.onError by including
err.message (and optionally err or String(err) as a details field) in the object
sent to callbacks?.onError?. so the error payload contains both the
WEBSOCKET_ERROR code and the actual error text for debugging.
- Around line 287-304: The current Promise uses polling over the `status`
variable to wait for the WebSocket connection; replace it with an event-driven
wait that listens for the WebSocket instance's lifecycle events instead. Inside
the same async block replacing the polling Promise, attach `ws.once("open",
...)` to resolve and `ws.once("close", ...)` and `ws.once("error", ...)` to
reject, keep the existing timeout fallback (clear it on resolve/reject), and
remove the setTimeout polling/checkOpen loop; reference the `status` usage and
the Promise wrapping the connection wait so the logic is driven by `ws` events
rather than repeated `setTimeout` checks.
In `@src/adapters/soniox/index.ts`:
- Line 1: The adapter is not re-exporting the generated Soniox API, so create a
new exports.ts that re-exports the generated functions and types (e.g.,
createTranscription, getTranscription, deleteTranscription, getFiles,
uploadFile, getFile, and their related types) from the generated sonioxPublicAPI
module, then update index.ts to export everything from this new exports.ts so
the functions become part of the adapter's public API; ensure the exports mirror
the AssemblyAI pattern (selective re-exports) and include the exact generated
symbols named above.
In `@src/adapters/soniox/mappers.ts`:
- Around line 83-85: The code redundantly filters response.tokens by is_final
before calling tokensToUtterances; remove that pre-filter and let
tokensToUtterances (or tokensToWords if you've adopted that helper) perform the
is_final filtering internally. Specifically, update the call that creates
utterances (currently using tokensToUtterances(response.tokens.filter((t) =>
t.is_final))) to just pass response.tokens, and ensure tokensToUtterances or
tokensToWords contains the is_final filter logic so the caller no longer needs
to filter.
- Around line 26-47: tokensToUtterances is inconsistent with tokensToWords:
update tokensToUtterances to filter tokens the same way as tokensToWords (only
include t.is_final and t.start_ms/end_ms defined) before mapping, and stop
defaulting missing times to 0; map start and end by dividing start_ms/end_ms by
1000 (same logic as tokensToWords) and then call buildUtterancesFromWords so
both functions produce the same subset of tokens/words.
- Around line 57-64: The current text assembly in mappers.ts uses response.text
or concatenates response.tokens with an empty separator, which can create words
run together; update the fallback path (the expression using response.tokens and
.filter on token.is_final) to trim each token's text and join tokens with a
single space, then trim the final result so extra spaces are removed (i.e., map
tokens via token.text.trim(), join with " ", and trim the combined string) so
the constructed variable text contains properly spaced words; ensure this change
is applied where the const text is created in the Soniox mapper.
In `@src/adapters/speechmatics/adapter.ts`:
- Around line 98-103: The URL branch currently sends JSON but the API contract
and generated postJobs expect multipart FormData; update the URL branch in
adapter.ts to build and send FormData like the file branch (append 'config' with
JSON.stringify(jobConfig) and any other fields the generated postJobs expects)
and remove the manual Content-Type header so the multipart boundary is set
correctly (or simply call the generated postJobs from
src/generated/speechmatics/api/speechmaticsASRRESTAPI.ts instead of crafting the
request manually). Ensure you reference and reuse jobConfig, audio, requestBody
(as FormData), and the generated postJobs function so both URL and file branches
construct identical FormData payloads.
In `@src/adapters/speechmatics/mappers.ts`:
- Around line 49-53: Add an inline comment above the block that sets
jobConfig.transcription_config!.speaker_diarization_config explaining the
heuristic used to compute speaker_sensitivity from options.speakersExpected:
document that speaker_sensitivity = Math.min(1, speakersExpected / 10) maps
expected speakers to sensitivity (e.g., 1 -> 0.1, 5 -> 0.5, 10+ -> 1.0), why we
clamp at 1, and any assumptions or trade-offs; reference the symbols
options.speakersExpected and speaker_sensitivity in the comment so future
maintainers understand the rationale and can adjust the divisor or clamp if
needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c682d618-c799-463f-af11-d1f58823185b
📒 Files selected for processing (46)
src/adapters/assemblyai-adapter.tssrc/adapters/assemblyai/adapter.tssrc/adapters/assemblyai/exports.tssrc/adapters/assemblyai/helpers.tssrc/adapters/assemblyai/index.tssrc/adapters/assemblyai/mappers.tssrc/adapters/azure-stt-adapter.tssrc/adapters/azure-stt/adapter.tssrc/adapters/azure-stt/exports.tssrc/adapters/azure-stt/index.tssrc/adapters/azure-stt/mappers.tssrc/adapters/deepgram-adapter.tssrc/adapters/deepgram/adapter.tssrc/adapters/deepgram/exports.tssrc/adapters/deepgram/helpers.tssrc/adapters/deepgram/index.tssrc/adapters/deepgram/mappers.tssrc/adapters/elevenlabs-adapter.tssrc/adapters/elevenlabs/adapter.tssrc/adapters/elevenlabs/helpers.tssrc/adapters/elevenlabs/index.tssrc/adapters/elevenlabs/mappers.tssrc/adapters/gladia-adapter.tssrc/adapters/gladia/adapter.tssrc/adapters/gladia/exports.tssrc/adapters/gladia/helpers.tssrc/adapters/gladia/index.tssrc/adapters/gladia/mappers.tssrc/adapters/index.tssrc/adapters/openai-whisper-adapter.tssrc/adapters/openai-whisper/adapter.tssrc/adapters/openai-whisper/exports.tssrc/adapters/openai-whisper/helpers.tssrc/adapters/openai-whisper/index.tssrc/adapters/openai-whisper/mappers.tssrc/adapters/provider-endpoints.tssrc/adapters/shared-types.tssrc/adapters/soniox-adapter.tssrc/adapters/soniox/adapter.tssrc/adapters/soniox/helpers.tssrc/adapters/soniox/index.tssrc/adapters/soniox/mappers.tssrc/adapters/speechmatics-adapter.tssrc/adapters/speechmatics/adapter.tssrc/adapters/speechmatics/index.tssrc/adapters/speechmatics/mappers.ts
| audioBuffer = Buffer.concat([audioBuffer, chunk.data]) | ||
| if (audioBuffer.length >= MIN_CHUNK_SIZE || audioBuffer.length >= MAX_CHUNK_SIZE) { | ||
| if (callbacks?.onRawMessage) { | ||
| const audioPayload = audioBuffer.buffer.slice( | ||
| audioBuffer.byteOffset, | ||
| audioBuffer.byteOffset + audioBuffer.byteLength | ||
| ) | ||
| callbacks.onRawMessage({ | ||
| provider, | ||
| direction: "outgoing", | ||
| timestamp: Date.now(), | ||
| payload: audioPayload, | ||
| messageType: "audio" | ||
| }) | ||
| } | ||
|
|
||
| ws.send(audioBuffer) | ||
| audioBuffer = Buffer.alloc(0) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Redundant condition in chunk size check.
The condition audioBuffer.length >= MIN_CHUNK_SIZE || audioBuffer.length >= MAX_CHUNK_SIZE is redundant—if length ≥ MAX_CHUNK_SIZE (32000), it's also ≥ MIN_CHUNK_SIZE (1600). The logic effectively just checks >= MIN_CHUNK_SIZE.
If the intent is to always send at MIN threshold, the MAX check is unnecessary. If MAX should enforce immediate sending regardless of other conditions, consider restructuring the logic.
🧹 Simplified condition
audioBuffer = Buffer.concat([audioBuffer, chunk.data])
- if (audioBuffer.length >= MIN_CHUNK_SIZE || audioBuffer.length >= MAX_CHUNK_SIZE) {
+ if (audioBuffer.length >= MIN_CHUNK_SIZE) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| audioBuffer = Buffer.concat([audioBuffer, chunk.data]) | |
| if (audioBuffer.length >= MIN_CHUNK_SIZE || audioBuffer.length >= MAX_CHUNK_SIZE) { | |
| if (callbacks?.onRawMessage) { | |
| const audioPayload = audioBuffer.buffer.slice( | |
| audioBuffer.byteOffset, | |
| audioBuffer.byteOffset + audioBuffer.byteLength | |
| ) | |
| callbacks.onRawMessage({ | |
| provider, | |
| direction: "outgoing", | |
| timestamp: Date.now(), | |
| payload: audioPayload, | |
| messageType: "audio" | |
| }) | |
| } | |
| ws.send(audioBuffer) | |
| audioBuffer = Buffer.alloc(0) | |
| } | |
| audioBuffer = Buffer.concat([audioBuffer, chunk.data]) | |
| if (audioBuffer.length >= MIN_CHUNK_SIZE) { | |
| if (callbacks?.onRawMessage) { | |
| const audioPayload = audioBuffer.buffer.slice( | |
| audioBuffer.byteOffset, | |
| audioBuffer.byteOffset + audioBuffer.byteLength | |
| ) | |
| callbacks.onRawMessage({ | |
| provider, | |
| direction: "outgoing", | |
| timestamp: Date.now(), | |
| payload: audioPayload, | |
| messageType: "audio" | |
| }) | |
| } | |
| ws.send(audioBuffer) | |
| audioBuffer = Buffer.alloc(0) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/assemblyai/helpers.ts` around lines 189 - 207, The condition
checking chunk size is redundant: replace the current "audioBuffer.length >=
MIN_CHUNK_SIZE || audioBuffer.length >= MAX_CHUNK_SIZE" with a single check
against MIN_CHUNK_SIZE (i.e., "audioBuffer.length >= MIN_CHUNK_SIZE"); if you
intended MAX_CHUNK_SIZE to force an immediate flush, instead handle it
explicitly by checking "audioBuffer.length >= MAX_CHUNK_SIZE" first and then
fall back to "audioBuffer.length >= MIN_CHUNK_SIZE". Update the check around
audioBuffer (used with callbacks.onRawMessage and ws.send) accordingly.
| const keyterms = options?.customVocabulary || aaiOpts.keyterms | ||
| if (keyterms?.length) keyterms.forEach((t) => params.append("keyterms", t)) | ||
| if (aaiOpts.keytermsPrompt?.length) { | ||
| aaiOpts.keytermsPrompt.forEach((p) => params.append("keyterms_prompt", p)) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Address Biome lint warnings for forEach callbacks.
Same issue as in deepgram/mappers.ts - the single-expression arrow functions implicitly return values.
Proposed fix
const keyterms = options?.customVocabulary || aaiOpts.keyterms
- if (keyterms?.length) keyterms.forEach((t) => params.append("keyterms", t))
+ if (keyterms?.length) keyterms.forEach((t) => { params.append("keyterms", t) })
if (aaiOpts.keytermsPrompt?.length) {
- aaiOpts.keytermsPrompt.forEach((p) => params.append("keyterms_prompt", p))
+ aaiOpts.keytermsPrompt.forEach((p) => { params.append("keyterms_prompt", p) })
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const keyterms = options?.customVocabulary || aaiOpts.keyterms | |
| if (keyterms?.length) keyterms.forEach((t) => params.append("keyterms", t)) | |
| if (aaiOpts.keytermsPrompt?.length) { | |
| aaiOpts.keytermsPrompt.forEach((p) => params.append("keyterms_prompt", p)) | |
| } | |
| const keyterms = options?.customVocabulary || aaiOpts.keyterms | |
| if (keyterms?.length) keyterms.forEach((t) => { params.append("keyterms", t) }) | |
| if (aaiOpts.keytermsPrompt?.length) { | |
| aaiOpts.keytermsPrompt.forEach((p) => { params.append("keyterms_prompt", p) }) | |
| } |
🧰 Tools
🪛 Biome (2.4.7)
[error] 245-245: This callback passed to forEach() iterable method should not return a value.
(lint/suspicious/useIterableCallbackReturn)
[error] 247-247: This callback passed to forEach() iterable method should not return a value.
(lint/suspicious/useIterableCallbackReturn)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/assemblyai/mappers.ts` around lines 244 - 248, The forEach
callbacks in the keyterms and keytermsPrompt handling (variables keyterms and
aaiOpts.keytermsPrompt, and calls to params.append) produce implicit returns
flagged by Biome; replace those forEach usages with explicit loops (e.g.,
for...of) or convert the arrow callbacks to block bodies that call params.append
without returning a value so there is no implicit return from the callback.
Locate the code around keyterms = options?.customVocabulary || aaiOpts.keyterms
and the aaiOpts.keytermsPrompt.forEach(...) and change them accordingly to avoid
implicit return values.
| const contentResponse = await axios.get(resultFile.links.contentUrl) | ||
| return mapFromAzureResponse(transcription, contentResponse.data, this.name) |
There was a problem hiding this comment.
Use the configured timeout for result-file downloads.
This bare axios.get() bypasses the adapter timeout, so a stalled blob download can hang getTranscript() and every pollForCompletion() caller indefinitely.
🔧 Proposed fix
- const contentResponse = await axios.get(resultFile.links.contentUrl)
+ const contentResponse = await axios.get(resultFile.links.contentUrl, {
+ timeout: this.getAxiosConfig().timeout
+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const contentResponse = await axios.get(resultFile.links.contentUrl) | |
| return mapFromAzureResponse(transcription, contentResponse.data, this.name) | |
| const contentResponse = await axios.get(resultFile.links.contentUrl, { | |
| timeout: this.getAxiosConfig().timeout | |
| }) | |
| return mapFromAzureResponse(transcription, contentResponse.data, this.name) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/azure-stt/adapter.ts` around lines 166 - 167, The axios.get used
when downloading resultFile content in adapter.ts bypasses the adapter's
configured timeout and can hang getTranscript() and pollForCompletion(); update
the download call to use the adapter's configured timeout (or the adapter's
axios/http client that already has timeout configured) when fetching
resultFile.links.contentUrl so that the request honors the adapter timeout
settings before calling mapFromAzureResponse(transcription, ..., this.name).
| export function normalizeAzureStatus( | ||
| status: unknown | ||
| ): "queued" | "processing" | "completed" | "error" { | ||
| const statusStr = status?.toString().toLowerCase() || "" | ||
| if (statusStr.includes("succeeded")) return "completed" | ||
| if (statusStr.includes("running")) return "processing" | ||
| if (statusStr.includes("notstarted")) return "queued" | ||
| if (statusStr.includes("failed")) return "error" | ||
| return "queued" | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Reuse the shared status normalizer here.
The codebase already has a provider-aware normalizer that handles Azure-style substring matches, so keeping a second Azure-specific copy here invites drift. This version also falls back to "queued", which can turn unexpected provider statuses into polling timeouts instead of surfacing the mismatch quickly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/azure-stt/mappers.ts` around lines 16 - 25, Replace this
provider-specific copier normalizeAzureStatus with the shared provider-aware
normalizer: remove or stop exporting normalizeAzureStatus and instead import and
call the existing shared normalizer (the provider-aware status normalizer used
elsewhere) so Azure substring matches are handled centrally; update the call
sites to use that shared function and ensure you do not reintroduce the old
fallback of returning "queued" for unknown statuses (let the shared normalizer’s
fallback behavior remain in effect or change it there if needed).
| return { | ||
| transcripts, | ||
| hasMore: (data.page || 1) * (data.limit || 10) < (data.requests?.length || 0) | ||
| } |
There was a problem hiding this comment.
Incorrect hasMore pagination logic.
The calculation (data.page || 1) * (data.limit || 10) < (data.requests?.length || 0) is inverted. For page=1, limit=10, and 10 items returned, this evaluates to 10 < 10 = false, incorrectly indicating no more results.
The standard pagination heuristic is: if the returned count equals the limit, there may be more items.
Proposed fix
return {
transcripts,
- hasMore: (data.page || 1) * (data.limit || 10) < (data.requests?.length || 0)
+ hasMore: (data.requests?.length || 0) >= (data.limit || 10)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return { | |
| transcripts, | |
| hasMore: (data.page || 1) * (data.limit || 10) < (data.requests?.length || 0) | |
| } | |
| return { | |
| transcripts, | |
| hasMore: (data.requests?.length || 0) >= (data.limit || 10) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/deepgram/adapter.ts` around lines 200 - 203, The hasMore
pagination check is inverted; replace the expression using
data.page/data.requests with a check comparing the number of returned items to
the page limit: compute the effective limit (const limit = data.limit || 10) and
set hasMore based on whether transcripts.length equals that limit (e.g., hasMore
= transcripts.length === limit) so when a full page of results is returned we
mark there may be more; update the return that contains transcripts and hasMore
accordingly (refer to transcripts, hasMore, data.limit, and
data.requests?.length in adapter.ts).
| function tokensToWords(tokens: SonioxToken[]): Word[] { | ||
| return tokens | ||
| .filter((t) => t.is_final && t.start_ms !== undefined && t.end_ms !== undefined) | ||
| .map((token) => ({ | ||
| word: token.text || "", | ||
| start: token.start_ms! / 1000, | ||
| end: token.end_ms! / 1000, | ||
| confidence: token.confidence, | ||
| speaker: token.speaker | ||
| })) | ||
| } | ||
|
|
||
| function tokensToUtterances(tokens: SonioxToken[]): Utterance[] { | ||
| const words = tokens.map((token) => ({ | ||
| word: token.text || "", | ||
| start: token.start_ms ? token.start_ms / 1000 : 0, | ||
| end: token.end_ms ? token.end_ms / 1000 : 0, | ||
| confidence: token.confidence, | ||
| speaker: token.speaker | ||
| })) | ||
| return buildUtterancesFromWords(words) | ||
| } |
There was a problem hiding this comment.
Inconsistent token filtering between tokensToWords and tokensToUtterances.
tokensToWords (lines 26-36) filters tokens by is_final and requires defined start_ms/end_ms, while tokensToUtterances (lines 38-47) applies no filtering and defaults missing times to 0. This inconsistency means words and utterances in the unified response could represent different subsets of tokens.
Consider aligning both functions to use the same filtering logic:
🛠️ Proposed fix to align filtering
function tokensToUtterances(tokens: SonioxToken[]): Utterance[] {
- const words = tokens.map((token) => ({
- word: token.text || "",
- start: token.start_ms ? token.start_ms / 1000 : 0,
- end: token.end_ms ? token.end_ms / 1000 : 0,
- confidence: token.confidence,
- speaker: token.speaker
- }))
+ const words = tokensToWords(tokens)
return buildUtterancesFromWords(words)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function tokensToWords(tokens: SonioxToken[]): Word[] { | |
| return tokens | |
| .filter((t) => t.is_final && t.start_ms !== undefined && t.end_ms !== undefined) | |
| .map((token) => ({ | |
| word: token.text || "", | |
| start: token.start_ms! / 1000, | |
| end: token.end_ms! / 1000, | |
| confidence: token.confidence, | |
| speaker: token.speaker | |
| })) | |
| } | |
| function tokensToUtterances(tokens: SonioxToken[]): Utterance[] { | |
| const words = tokens.map((token) => ({ | |
| word: token.text || "", | |
| start: token.start_ms ? token.start_ms / 1000 : 0, | |
| end: token.end_ms ? token.end_ms / 1000 : 0, | |
| confidence: token.confidence, | |
| speaker: token.speaker | |
| })) | |
| return buildUtterancesFromWords(words) | |
| } | |
| function tokensToWords(tokens: SonioxToken[]): Word[] { | |
| return tokens | |
| .filter((t) => t.is_final && t.start_ms !== undefined && t.end_ms !== undefined) | |
| .map((token) => ({ | |
| word: token.text || "", | |
| start: token.start_ms! / 1000, | |
| end: token.end_ms! / 1000, | |
| confidence: token.confidence, | |
| speaker: token.speaker | |
| })) | |
| } | |
| function tokensToUtterances(tokens: SonioxToken[]): Utterance[] { | |
| const words = tokensToWords(tokens) | |
| return buildUtterancesFromWords(words) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/soniox/mappers.ts` around lines 26 - 47, tokensToUtterances is
inconsistent with tokensToWords: update tokensToUtterances to filter tokens the
same way as tokensToWords (only include t.is_final and t.start_ms/end_ms
defined) before mapping, and stop defaulting missing times to 0; map start and
end by dividing start_ms/end_ms by 1000 (same logic as tokensToWords) and then
call buildUtterancesFromWords so both functions produce the same subset of
tokens/words.
| const text = | ||
| response.text || | ||
| (response.tokens | ||
| ? response.tokens | ||
| .filter((t) => t.is_final) | ||
| .map((t) => t.text) | ||
| .join("") | ||
| : "") |
There was a problem hiding this comment.
Text concatenation may produce malformed output without spaces.
When response.text is absent, tokens are joined with empty string (.join("")). If token text fields don't include trailing/leading whitespace, this produces concatenated words without spaces (e.g., "HelloWorld" instead of "Hello World").
🔧 Proposed fix
const text =
response.text ||
(response.tokens
? response.tokens
.filter((t) => t.is_final)
.map((t) => t.text)
- .join("")
+ .join(" ")
: "")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/soniox/mappers.ts` around lines 57 - 64, The current text
assembly in mappers.ts uses response.text or concatenates response.tokens with
an empty separator, which can create words run together; update the fallback
path (the expression using response.tokens and .filter on token.is_final) to
trim each token's text and join tokens with a single space, then trim the final
result so extra spaces are removed (i.e., map tokens via token.text.trim(), join
with " ", and trim the combined string) so the constructed variable text
contains properly spaced words; ensure this change is applied where the const
text is created in the Soniox mapper.
| const utterances = response.tokens | ||
| ? tokensToUtterances(response.tokens.filter((t) => t.is_final)) | ||
| : [] |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Redundant filtering logic.
The caller filters tokens by is_final before passing to tokensToUtterances, but this filtering should be handled inside tokensToUtterances itself (or via tokensToWords if the suggested fix above is applied). With the proposed alignment, this pre-filter would become unnecessary since tokensToWords already filters by is_final.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/soniox/mappers.ts` around lines 83 - 85, The code redundantly
filters response.tokens by is_final before calling tokensToUtterances; remove
that pre-filter and let tokensToUtterances (or tokensToWords if you've adopted
that helper) perform the is_final filtering internally. Specifically, update the
call that creates utterances (currently using
tokensToUtterances(response.tokens.filter((t) => t.is_final))) to just pass
response.tokens, and ensure tokensToUtterances or tokensToWords contains the
is_final filter logic so the caller no longer needs to filter.
| if (audio.type === "url") { | ||
| jobConfig.fetch_data = { | ||
| url: audio.url | ||
| } | ||
| requestBody = { config: JSON.stringify(jobConfig) } | ||
| headers = { "Content-Type": "application/json" } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Current adapter implementation:"
sed -n '87,125p' src/adapters/speechmatics/adapter.ts
echo
echo "Generated Speechmatics create-job signatures:"
rg -n -C4 '\bcreateJob\b|/jobs' src/generated/speechmatics
echo
echo "Generated request fields related to job creation:"
rg -n -C4 '\bfetch_data\b|\bdata_file\b|\btranscription_config\b|\bconfig\b' src/generated/speechmaticsRepository: Meeting-BaaS/sdk-generator
Length of output: 50382
🏁 Script executed:
#!/bin/bash
# Get the postJobs implementation that the adapter should ideally be using
sed -n '40,65p' src/generated/speechmatics/api/speechmaticsASRRESTAPI.ts
# Check how the generated client handles the request
sed -n '1,70p' src/generated/speechmatics/api/speechmaticsASRRESTAPI.ts | grep -A 30 "export const postJobs"Repository: Meeting-BaaS/sdk-generator
Length of output: 1358
🏁 Script executed:
#!/bin/bash
# Check imports and client initialization in adapter
head -50 src/adapters/speechmatics/adapter.ts
# Check if postJobs is imported or available
rg -n "postJobs|import.*speechmatics" src/adapters/speechmatics/adapter.tsRepository: Meeting-BaaS/sdk-generator
Length of output: 2021
🏁 Script executed:
#!/bin/bash
# Check if postJobs is exported from generated API
rg -n "export.*postJobs" src/generated/speechmatics/api/speechmaticsASRRESTAPI.ts | head -5
# See the full function signature
sed -n '44,75p' src/generated/speechmatics/api/speechmaticsASRRESTAPI.tsRepository: Meeting-BaaS/sdk-generator
Length of output: 961
Fix the URL branch to use FormData, matching the generated API contract.
The generated postJobs function always uses FormData with multipart encoding for all requests, regardless of whether files are present. The URL branch currently sends plain JSON ({ config: JSON.stringify(jobConfig) }) with Content-Type: application/json, which violates the API contract and will fail. Both branches should use FormData.
Either import and use the generated postJobs function from src/generated/speechmatics/api/speechmaticsASRRESTAPI.ts, or construct FormData identically for both URL and file branches.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/speechmatics/adapter.ts` around lines 98 - 103, The URL branch
currently sends JSON but the API contract and generated postJobs expect
multipart FormData; update the URL branch in adapter.ts to build and send
FormData like the file branch (append 'config' with JSON.stringify(jobConfig)
and any other fields the generated postJobs expects) and remove the manual
Content-Type header so the multipart boundary is set correctly (or simply call
the generated postJobs from
src/generated/speechmatics/api/speechmaticsASRRESTAPI.ts instead of crafting the
request manually). Ensure you reference and reuse jobConfig, audio, requestBody
(as FormData), and the generated postJobs function so both URL and file branches
construct identical FormData payloads.
| if (options.speakersExpected) { | ||
| jobConfig.transcription_config!.speaker_diarization_config = { | ||
| speaker_sensitivity: Math.min(1, options.speakersExpected / 10) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Document the speaker sensitivity heuristic.
The formula Math.min(1, speakersExpected / 10) maps expected speaker count to sensitivity (0.1 for 1 speaker, 0.5 for 5, 1.0 for 10+). This non-obvious mapping should be documented to explain the rationale and help future maintainers.
📝 Suggested documentation
if (options?.diarization) {
jobConfig.transcription_config!.diarization = TranscriptionConfigDiarization.speaker
if (options.speakersExpected) {
+ // Map expected speakers to sensitivity: 1 speaker → 0.1, 5 → 0.5, 10+ → 1.0
+ // Higher sensitivity increases speaker separation aggressiveness
jobConfig.transcription_config!.speaker_diarization_config = {
speaker_sensitivity: Math.min(1, options.speakersExpected / 10)
}
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adapters/speechmatics/mappers.ts` around lines 49 - 53, Add an inline
comment above the block that sets
jobConfig.transcription_config!.speaker_diarization_config explaining the
heuristic used to compute speaker_sensitivity from options.speakersExpected:
document that speaker_sensitivity = Math.min(1, speakersExpected / 10) maps
expected speakers to sensitivity (e.g., 1 -> 0.1, 5 -> 0.5, 10+ -> 1.0), why we
clamp at 1, and any assumptions or trade-offs; reference the symbols
options.speakersExpected and speaker_sensitivity in the comment so future
maintainers understand the rationale and can adjust the divisor or clamp if
needed.
- Replace forEach with for...of to avoid implicit return lint warnings - Fix redundant chunk size condition in AssemblyAI helpers - Add timeout to bare axios.get in Azure STT adapter - Use shared normalizeStatus() in Azure STT mappers - Fix inverted hasMore pagination in Deepgram adapter - Replace polling-based WS wait with event-driven ws.once() (ElevenLabs, Soniox) - Await socket close with timeout in ElevenLabs close() - Remove redundant ?? undefined in ElevenLabs mappers - Preserve language_config spread in Gladia mappers - Replace as any with concrete type union in OpenAI adapter - Remove misleading onUtterance with placeholder timing in OpenAI helpers - Add generateRequestId() and fix fragile response shape check in OpenAI mappers - Fix Speechmatics websocket override passthrough in provider-endpoints - Remove redundant Soniox region fallback, throw on unknown provider - Remove try/catch swallowing errors in Soniox getModels() - Include err.message in Soniox WS error handler - Filter is_final + defined times in Soniox tokensToUtterances, fix text join - Use FormData for Speechmatics URL submissions, wrap Buffer in Blob - Add speaker_sensitivity heuristic comment in Speechmatics mappers - Fix ElevenLabs generated code FormData type mismatches - Create elevenlabs/exports.ts and soniox/exports.ts barrel files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use ?? instead of || for timeout fallbacks across all adapters (azure-stt, speechmatics, soniox, elevenlabs, deepgram, base-adapter) so explicit timeout: 0 is preserved - Deduplicate tokensToUtterances in soniox/mappers by calling tokensToWords instead of reimplementing the same filter+map - Use application/octet-stream and generic filename as Speechmatics audio fallbacks, warn when metadata is missing, add Content-Type header - Remove dead MAX_CHUNK_SIZE constant from assemblyai/helpers - Fix gladia language_config merge to preserve codeSwitchingConfig values for languages/code_switching when explicit options are absent - Remove redundant inner if(needsWords) in openai-whisper/mappers, use generateRequestId() in fallback branch - Re-add Soniox region fallback for invalid region strings - Make Soniox close() await WS shutdown with 5s timeout + terminate Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Orval generates .optional() on the type discriminator inside
z.discriminatedUnion('type', [...]) for OpenAI Realtime audio format
schemas, causing Zod to throw "duplicate value undefined" on import.
Strip .optional() from 8 discriminator fields so each variant has a
unique literal type value.
Also bumps version to 0.9.0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
*-adapter.tsfiles (800-1600 lines each) into per-provider folders:adapter.ts,helpers.ts,mappers.ts,exports.tsprovider-endpoints.ts(centralized region/URL logic) andshared-types.ts*-adapter.tsfiles become 1-lineexport *shims for backwards compatibilityStructure
Verified
pnpm build:bundlepasses cleanTest plan
pnpm build:bundle— no type errorsimport { GladiaAdapter, createAzureSTTAdapter } from 'voice-router-dev')voice-router-dev/webhooksstill resolve🤖 Generated with Claude Code