@@ -44,6 +44,13 @@ export interface TurnDetectorOptions {
4444 apiSecret ?: string ;
4545 /** Sample rate (Hz). Defaults to 16000. */
4646 sampleRate ?: number ;
47+ /**
48+ * Whether a `v1` detector may degrade to the local `v1-mini` model when the
49+ * gateway fails. `false` keeps it cloud-only, so the mini weights (~138 MB,
50+ * resident for the process lifetime) are never loaded and turns commit on
51+ * the endpointing delay instead.
52+ */
53+ localFallback ?: boolean ;
4754 connOptions ?: APIConnectOptions ;
4855 /**
4956 * Inference executor that runs the local `turn-detector-v1-mini` model in the
@@ -59,6 +66,7 @@ export class TurnDetector extends BaseStreamingTurnDetector {
5966 protected _model : TurnDetectorModel ;
6067 protected _cloudOpts : CloudTransportOptions | undefined ;
6168 protected _executor : InferenceExecutor | undefined ;
69+ protected _localFallback : boolean ;
6270
6371 constructor ( opts : TurnDetectorOptions = { } ) {
6472 // auto = caller didn't pin a version; missing cloud creds warn-and-
@@ -118,6 +126,13 @@ export class TurnDetector extends BaseStreamingTurnDetector {
118126 super ( detectorOpts ) ;
119127 this . _model = resolvedModel ;
120128 this . _cloudOpts = cloudOpts ;
129+ this . _localFallback = opts . localFallback ?? true ;
130+ if ( ! this . _localFallback && resolvedModel === 'turn-detector-v1-mini' ) {
131+ log ( ) . warn (
132+ { model : resolvedModel } ,
133+ 'localFallback=false has no effect on the local model, which runs locally by design' ,
134+ ) ;
135+ }
121136 this . _warnThresholdOverride ( ) ;
122137 // Default to the current job's shared inference executor. `getJobContext`
123138 // throws outside a job (tests, standalone) — degrade to `undefined`
@@ -208,6 +223,7 @@ export class TurnDetector extends BaseStreamingTurnDetector {
208223 opts : this . _opts ,
209224 cloudOpts,
210225 model : this . _model ,
226+ localFallback : this . _localFallback ,
211227 executor : this . _executor ,
212228 } ) ;
213229 this . _streams . add ( stream ) ;
@@ -220,6 +236,7 @@ export interface TurnDetectorStreamImplArgs {
220236 opts : BaseStreamingTurnDetectorOptions ;
221237 cloudOpts : CloudTransportOptions | undefined ;
222238 model : TurnDetectorModel ;
239+ localFallback ?: boolean ;
223240 /** Shared inference executor for the `turn-detector-v1-mini` (local) model
224241 * (undefined degrades to a positive-default prediction). */
225242 executor ?: InferenceExecutor ;
@@ -239,7 +256,9 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream {
239256 protected _model : TurnDetectorModel ;
240257 protected _cloudOpts : CloudTransportOptions | undefined ;
241258 protected _executor : InferenceExecutor | undefined ;
259+ protected _localFallback : boolean ;
242260 protected _isFallback = false ;
261+ protected _isDegraded = false ;
243262 protected _warnedCloudFailure = false ;
244263 protected _warnedLocalFailure = false ;
245264 private _detLogger = log ( ) ;
@@ -258,6 +277,7 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream {
258277 this . _model = args . model ;
259278 this . _cloudOpts = args . cloudOpts ;
260279 this . _executor = args . executor ;
280+ this . _localFallback = args . localFallback ?? true ;
261281 }
262282
263283 /** This stream's *current* model name (flips to `'turn-detector-v1-mini'`
@@ -271,6 +291,11 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream {
271291 return this . _isFallback ;
272292 }
273293
294+ /** True once the cloud transport is gone with no local model to replace it. */
295+ get isDegraded ( ) : boolean {
296+ return this . _isDegraded ;
297+ }
298+
274299 /** @internal Test-visible. */
275300 get warnedCloudFailure ( ) : boolean {
276301 return this . _warnedCloudFailure ;
@@ -287,7 +312,12 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream {
287312 /** @internal Test-visible: same logic as the path taken when `_run` catches
288313 * a cloud transport error. Tests call this directly to verify the warning
289314 * dedupe across multiple invocations on the same stream. */
290- _fallBackToLocal ( reason : Error ) : void {
315+ _fallbackToLocal ( reason : Error ) : boolean {
316+ if ( ! this . _localFallback ) {
317+ this . _emitDefaultForInflight ( ) ;
318+ return false ;
319+ }
320+
291321 if ( ! this . _warnedCloudFailure ) {
292322 this . _detLogger . warn (
293323 { reason : reason . message } ,
@@ -313,6 +343,22 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream {
313343 this . _transport . attach ( this ) ;
314344 this . _model = 'turn-detector-v1-mini' ;
315345 this . _isFallback = true ;
346+ return true ;
347+ }
348+
349+ protected _degrade ( reason : Error ) : void {
350+ this . _detLogger . warn (
351+ { reason : reason . message } ,
352+ 'cloud turn detector failed; local fallback is disabled, so turn detection is off for ' +
353+ 'the rest of this stream and turns commit on the endpointing delay' ,
354+ ) ;
355+ this . _isDegraded = true ;
356+ try {
357+ this . _transport . detach ( ) ;
358+ } catch {
359+ // ignore detach errors while terminating the failed transport
360+ }
361+ this . _discardAudioInput ( ) ;
316362 }
317363
318364 /** @internal Test-visible: same logic as the path taken when `_run` sees a
@@ -366,8 +412,11 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream {
366412 }
367413 const e = err instanceof Error ? err : new Error ( String ( err ) ) ;
368414 if ( this . _model === 'turn-detector-v1' ) {
369- this . _fallBackToLocal ( e ) ;
370- continue ;
415+ if ( this . _fallbackToLocal ( e ) ) {
416+ continue ;
417+ }
418+ this . _degrade ( e ) ;
419+ return ;
371420 }
372421 this . _onLocalFailure ( e ) ;
373422 return ;
@@ -377,13 +426,9 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream {
377426
378427 protected override _onPredictTimeout ( ) : void {
379428 if ( this . _model === 'turn-detector-v1' ) {
380- // Signal the swap BEFORE mutating model/transport state. The
381- // race in `_raceWithSwap` is rejected with `SwapAbortError`
382- // immediately, so the main loop exits through the
383- // SwapAbortError branch and never consults `_model` for a
384- // classification that would race with the assignment below.
385- this . _signalSwap ( ) ;
386- this . _fallBackToLocal ( new Error ( 'predict_end_of_turn' ) ) ;
429+ if ( this . _fallbackToLocal ( new Error ( 'predict_end_of_turn' ) ) ) {
430+ this . _signalSwap ( ) ;
431+ }
387432 }
388433 }
389434}
0 commit comments