realtime-voice-agents 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/dist/gemini.cjs +14 -5
- package/dist/gemini.d.cts +6 -1
- package/dist/gemini.d.mts +6 -1
- package/dist/gemini.mjs +14 -5
- package/dist/index.cjs +57 -11
- package/dist/index.d.cts +42 -1
- package/dist/index.d.mts +42 -1
- package/dist/index.mjs +57 -11
- package/dist/openai.cjs +14 -5
- package/dist/openai.d.cts +8 -1
- package/dist/openai.d.mts +8 -1
- package/dist/openai.mjs +14 -5
- package/dist/testing.cjs +29 -2
- package/dist/testing.d.cts +30 -0
- package/dist/testing.d.mts +30 -0
- package/dist/testing.mjs +29 -2
- package/dist/xai.cjs +14 -5
- package/dist/xai.d.cts +6 -1
- package/dist/xai.d.mts +6 -1
- package/dist/xai.mjs +14 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,26 @@ geminiLive({ model: 'gemini-2.5-flash-native-audio-preview-12-2025', voice: 'Aoe
|
|
|
104
104
|
|
|
105
105
|
One `SessionOptions` surface configures all three; where a provider can't honor a knob, the fallback is documented and pinned by the parity test suite.
|
|
106
106
|
|
|
107
|
+
## Provider fallbacks
|
|
108
|
+
|
|
109
|
+
One bad API key, an exhausted quota, or a provider outage should not send your calls to dead air. Give the bridge backup providers and it tries them in order while the call is being established:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const bridge = new TwilioRealtimeBridge({
|
|
113
|
+
agent,
|
|
114
|
+
provider: openaiRealtime(), // primary
|
|
115
|
+
fallbacks: [xaiRealtime(), geminiLive()], // tried in order if it fails to come up
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- **Covers the real failure modes.** A missing API key (the factories defer their credential check to call time precisely so the chain can absorb it), an expired/revoked key (HTTP 401), exhausted credits/quota (403/429), a provider internal error (5xx or a dropped socket), and a hung endpoint (connect timeout) all walk the chain — anything that keeps a provider from coming up.
|
|
120
|
+
- **Connect-time only.** A dead provider is dropped and the next one is tried immediately — no backoff between attempts. Once a provider answers, the call stays with it: mid-call reconnects reuse the same provider (per the `session.reconnect` policy), and a mid-call death past that budget fails the call rather than switching voices mid-conversation.
|
|
121
|
+
- **Observable.** Each advance emits `provider.fallback` (`{ from, to, error }`) on the session — count these to alarm on a degraded primary.
|
|
122
|
+
- **Voices don't cross vendors.** Configure the voice per factory (`openaiRealtime({ voice: 'marin' })`, `xaiRealtime({ voice: 'eve' })`) rather than on the `Agent` — an OpenAI voice name would fail the xAI/Gemini connect and the chain would skip past a healthy provider.
|
|
123
|
+
- **Latency.** Each dead provider costs up to its `connectTimeoutMs` (default 10s) before the next is tried — set a tighter one on the primary if its endpoint tends to hang rather than refuse. A [pre-synthesized greeting](#pre-synthesized-greeting-15s-to-first-word) bursts onto the line before any handshake, so the caller hears a voice while the chain walks.
|
|
124
|
+
|
|
125
|
+
Testing it: `FakeOpenAIServer.start({ refuseConnections: true })` gives you a provider that is "down", and `{ rejectUpgrade: { status: 401, body: 'invalid_api_key' } }` one that rejects like a real auth/quota failure (both flippable at runtime to script recoveries) — see `src/bridge/fallback.test.ts` for ready-made scenarios.
|
|
126
|
+
|
|
107
127
|
## Tools: Zod schemas + execution strategies
|
|
108
128
|
|
|
109
129
|
```ts
|
|
@@ -252,7 +272,7 @@ Bundled presets (all synthesized, license-free, seamless loops): `elevator-jazz`
|
|
|
252
272
|
|
|
253
273
|
## Events (session)
|
|
254
274
|
|
|
255
|
-
`call.started/ended/failed` · `provider.connected/reconnecting/reconnected/closed` · `agent.speech.started/ended` (generation) · **`playback.started/finished/interrupted`** (what the caller heard, mark-confirmed) · `user.speech.started/ended` · `transcript.user/agent` · `tool.started/completed/failed` · `tool.approval.required` · `agent.handoff` · `interruption` / `interruption.blocked` · `vad.suggestion` / `vad.adjusted` (noise-adaptive VAD) · `background_audio.started/stopped` · `dtmf` · `usage.updated` · `error`.
|
|
275
|
+
`call.started/ended/failed` · `provider.connected/fallback/reconnecting/reconnected/closed` · `agent.speech.started/ended` (generation) · **`playback.started/finished/interrupted`** (what the caller heard, mark-confirmed) · `user.speech.started/ended` · `transcript.user/agent` · `tool.started/completed/failed` · `tool.approval.required` · `agent.handoff` · `interruption` / `interruption.blocked` · `vad.suggestion` / `vad.adjusted` (noise-adaptive VAD) · `background_audio.started/stopped` · `dtmf` · `usage.updated` · `error`.
|
|
256
276
|
|
|
257
277
|
```ts
|
|
258
278
|
bridge.on('session.started', (session) => {
|
|
@@ -296,7 +316,7 @@ Outbound calls: the greeting waits for a human — feed your status callback int
|
|
|
296
316
|
`realtime-voice-agents/testing` ships the harness this package is tested with:
|
|
297
317
|
|
|
298
318
|
- **`FakeTwilioMediaStream`** — a scripted caller with an exact playout simulation: marks echo only after the media before them "plays"; `clear` discards buffered audio and echoes pending marks, like real Twilio.
|
|
299
|
-
- **`FakeOpenAIServer`** — a real-WebSocket GA-protocol server you script (`sendAudioResponse`, `sendToolCall`, `sendSpeechStarted`, drops).
|
|
319
|
+
- **`FakeOpenAIServer`** — a real-WebSocket GA-protocol server you script (`sendAudioResponse`, `sendToolCall`, `sendSpeechStarted`, drops, `refuseConnections` for down-provider/fallback scenarios).
|
|
300
320
|
- **`FakeGeminiLive`** — a scripted `@google/genai` seam for the Gemini provider.
|
|
301
321
|
|
|
302
322
|
```ts
|
package/dist/gemini.cjs
CHANGED
|
@@ -373,12 +373,14 @@ const GEMINI_VOICES = [
|
|
|
373
373
|
"Kore",
|
|
374
374
|
"Puck"
|
|
375
375
|
];
|
|
376
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* Create a Gemini Live provider factory for the bridge. Credentials are
|
|
378
|
+
* resolved per call (see `openaiRealtime` — missing credentials fail that
|
|
379
|
+
* call's connect so a fallback chain can absorb it, instead of crashing
|
|
380
|
+
* config construction).
|
|
381
|
+
*/
|
|
377
382
|
function geminiLive(options = {}) {
|
|
378
|
-
const apiKey = require_env.resolveApiKey(options.apiKey, GEMINI_KEY_ENV_VARS);
|
|
379
|
-
if (!apiKey && !options.vertex && !options.connector) throw new Error(`geminiLive: credentials missing (pass apiKey, set one of ${GEMINI_KEY_ENV_VARS.join("/")}, or configure vertex)`);
|
|
380
383
|
const config = {
|
|
381
|
-
apiKey,
|
|
382
384
|
vertex: options.vertex,
|
|
383
385
|
model: options.model ?? "gemini-2.5-flash-native-audio-preview-12-2025",
|
|
384
386
|
voice: options.voice ?? "Aoede",
|
|
@@ -391,7 +393,14 @@ function geminiLive(options = {}) {
|
|
|
391
393
|
connectTimeoutMs: options.connectTimeoutMs,
|
|
392
394
|
connector: options.connector
|
|
393
395
|
};
|
|
394
|
-
return ({ logger }) =>
|
|
396
|
+
return ({ logger }) => {
|
|
397
|
+
const apiKey = require_env.resolveApiKey(options.apiKey, GEMINI_KEY_ENV_VARS);
|
|
398
|
+
if (!apiKey && !options.vertex && !options.connector) throw new Error(`geminiLive: credentials missing (pass apiKey, set one of ${GEMINI_KEY_ENV_VARS.join("/")}, or configure vertex)`);
|
|
399
|
+
return new GeminiLiveProvider({
|
|
400
|
+
...config,
|
|
401
|
+
apiKey
|
|
402
|
+
}, logger);
|
|
403
|
+
};
|
|
395
404
|
}
|
|
396
405
|
//#endregion
|
|
397
406
|
exports.GEMINI_DEFAULT_MODEL = GEMINI_DEFAULT_MODEL;
|
package/dist/gemini.d.cts
CHANGED
|
@@ -35,7 +35,12 @@ interface GeminiLiveOptions {
|
|
|
35
35
|
/** Test seam: replaces the @google/genai live connection. */
|
|
36
36
|
connector?: GeminiLiveConnector;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Create a Gemini Live provider factory for the bridge. Credentials are
|
|
40
|
+
* resolved per call (see `openaiRealtime` — missing credentials fail that
|
|
41
|
+
* call's connect so a fallback chain can absorb it, instead of crashing
|
|
42
|
+
* config construction).
|
|
43
|
+
*/
|
|
39
44
|
declare function geminiLive(options?: GeminiLiveOptions): ProviderFactory;
|
|
40
45
|
//#endregion
|
|
41
46
|
export { GEMINI_DEFAULT_MODEL, GEMINI_DEFAULT_VOICE, GEMINI_KEY_ENV_VARS, GEMINI_VOICES, type GeminiConnectParams, type GeminiLiveConnector, GeminiLiveOptions, GeminiLiveProvider, type GeminiLiveProviderConfig, type GeminiLiveSessionLike, geminiLive };
|
package/dist/gemini.d.mts
CHANGED
|
@@ -35,7 +35,12 @@ interface GeminiLiveOptions {
|
|
|
35
35
|
/** Test seam: replaces the @google/genai live connection. */
|
|
36
36
|
connector?: GeminiLiveConnector;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Create a Gemini Live provider factory for the bridge. Credentials are
|
|
40
|
+
* resolved per call (see `openaiRealtime` — missing credentials fail that
|
|
41
|
+
* call's connect so a fallback chain can absorb it, instead of crashing
|
|
42
|
+
* config construction).
|
|
43
|
+
*/
|
|
39
44
|
declare function geminiLive(options?: GeminiLiveOptions): ProviderFactory;
|
|
40
45
|
//#endregion
|
|
41
46
|
export { GEMINI_DEFAULT_MODEL, GEMINI_DEFAULT_VOICE, GEMINI_KEY_ENV_VARS, GEMINI_VOICES, type GeminiConnectParams, type GeminiLiveConnector, GeminiLiveOptions, GeminiLiveProvider, type GeminiLiveProviderConfig, type GeminiLiveSessionLike, geminiLive };
|
package/dist/gemini.mjs
CHANGED
|
@@ -372,12 +372,14 @@ const GEMINI_VOICES = [
|
|
|
372
372
|
"Kore",
|
|
373
373
|
"Puck"
|
|
374
374
|
];
|
|
375
|
-
/**
|
|
375
|
+
/**
|
|
376
|
+
* Create a Gemini Live provider factory for the bridge. Credentials are
|
|
377
|
+
* resolved per call (see `openaiRealtime` — missing credentials fail that
|
|
378
|
+
* call's connect so a fallback chain can absorb it, instead of crashing
|
|
379
|
+
* config construction).
|
|
380
|
+
*/
|
|
376
381
|
function geminiLive(options = {}) {
|
|
377
|
-
const apiKey = resolveApiKey(options.apiKey, GEMINI_KEY_ENV_VARS);
|
|
378
|
-
if (!apiKey && !options.vertex && !options.connector) throw new Error(`geminiLive: credentials missing (pass apiKey, set one of ${GEMINI_KEY_ENV_VARS.join("/")}, or configure vertex)`);
|
|
379
382
|
const config = {
|
|
380
|
-
apiKey,
|
|
381
383
|
vertex: options.vertex,
|
|
382
384
|
model: options.model ?? "gemini-2.5-flash-native-audio-preview-12-2025",
|
|
383
385
|
voice: options.voice ?? "Aoede",
|
|
@@ -390,7 +392,14 @@ function geminiLive(options = {}) {
|
|
|
390
392
|
connectTimeoutMs: options.connectTimeoutMs,
|
|
391
393
|
connector: options.connector
|
|
392
394
|
};
|
|
393
|
-
return ({ logger }) =>
|
|
395
|
+
return ({ logger }) => {
|
|
396
|
+
const apiKey = resolveApiKey(options.apiKey, GEMINI_KEY_ENV_VARS);
|
|
397
|
+
if (!apiKey && !options.vertex && !options.connector) throw new Error(`geminiLive: credentials missing (pass apiKey, set one of ${GEMINI_KEY_ENV_VARS.join("/")}, or configure vertex)`);
|
|
398
|
+
return new GeminiLiveProvider({
|
|
399
|
+
...config,
|
|
400
|
+
apiKey
|
|
401
|
+
}, logger);
|
|
402
|
+
};
|
|
394
403
|
}
|
|
395
404
|
//#endregion
|
|
396
405
|
export { GEMINI_DEFAULT_MODEL, GEMINI_DEFAULT_VOICE, GEMINI_KEY_ENV_VARS, GEMINI_VOICES, GeminiLiveProvider, geminiLive };
|
package/dist/index.cjs
CHANGED
|
@@ -907,6 +907,8 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
907
907
|
runningTools = /* @__PURE__ */ new Map();
|
|
908
908
|
timers = /* @__PURE__ */ new Set();
|
|
909
909
|
provider = null;
|
|
910
|
+
/** Primary + fallbacks, in try-order. Only walked while connecting. */
|
|
911
|
+
providerChain;
|
|
910
912
|
activeAgentValue;
|
|
911
913
|
generating = false;
|
|
912
914
|
currentResponseId = null;
|
|
@@ -965,6 +967,7 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
965
967
|
this.callSid = deps.start.start.callSid;
|
|
966
968
|
this.streamSid = deps.start.start.streamSid ?? deps.start.streamSid;
|
|
967
969
|
this.log = require_BaseRealtimeProvider.childLogger(deps.logger, { callSid: this.callSid });
|
|
970
|
+
this.providerChain = [deps.providerFactory, ...deps.fallbacks ?? []];
|
|
968
971
|
this.activeAgentValue = deps.agent;
|
|
969
972
|
this.context = new SessionContext(deps.options.context);
|
|
970
973
|
this.interruptions = new InterruptionController(deps.options.interruptions);
|
|
@@ -1003,17 +1006,7 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
1003
1006
|
/** Connect the provider and activate the call. Called by the bridge. */
|
|
1004
1007
|
async begin() {
|
|
1005
1008
|
this.playPreGreeting();
|
|
1006
|
-
|
|
1007
|
-
this.provider = this.deps.providerFactory({
|
|
1008
|
-
logger: this.log,
|
|
1009
|
-
callSid: this.callSid
|
|
1010
|
-
});
|
|
1011
|
-
this.wireProvider(this.provider);
|
|
1012
|
-
await this.provider.connect(this.buildProviderInit());
|
|
1013
|
-
} catch (error) {
|
|
1014
|
-
this.fail(error instanceof Error ? error : new Error(String(error)), "provider-failed");
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1009
|
+
if (!await this.connectInitialProvider()) return;
|
|
1017
1010
|
if (this.stateValue !== "connecting") return;
|
|
1018
1011
|
this.stateValue = "active";
|
|
1019
1012
|
this.emit("provider.connected");
|
|
@@ -1039,6 +1032,55 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
1039
1032
|
this.saveSnapshot();
|
|
1040
1033
|
this.maybeGreet();
|
|
1041
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Walk primary + fallbacks until one connects. A provider that fails to
|
|
1037
|
+
* come up is detached BEFORE the chain advances, so a half-open socket's
|
|
1038
|
+
* late events can never reach the session once the next provider owns the
|
|
1039
|
+
* call. Connect-time only by design: once a provider has answered, the
|
|
1040
|
+
* call stays with it. Returns false after failing the call (chain
|
|
1041
|
+
* exhausted) or when the call was torn down while connecting.
|
|
1042
|
+
*/
|
|
1043
|
+
async connectInitialProvider() {
|
|
1044
|
+
let lastFrom = "provider";
|
|
1045
|
+
let lastError = null;
|
|
1046
|
+
for (const factory of this.providerChain) {
|
|
1047
|
+
let provider;
|
|
1048
|
+
try {
|
|
1049
|
+
provider = factory({
|
|
1050
|
+
logger: this.log,
|
|
1051
|
+
callSid: this.callSid
|
|
1052
|
+
});
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
lastError = toError(error);
|
|
1055
|
+
this.log.warn("provider factory threw — trying the next fallback", { error: String(lastError) });
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (lastError) this.emit("provider.fallback", {
|
|
1059
|
+
from: lastFrom,
|
|
1060
|
+
to: provider.name,
|
|
1061
|
+
error: lastError
|
|
1062
|
+
});
|
|
1063
|
+
this.provider = provider;
|
|
1064
|
+
this.wireProvider(provider);
|
|
1065
|
+
try {
|
|
1066
|
+
await provider.connect(this.buildProviderInit());
|
|
1067
|
+
return true;
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
provider.removeAllListeners();
|
|
1070
|
+
provider.close().catch(() => {});
|
|
1071
|
+
this.provider = null;
|
|
1072
|
+
if (this.stateValue !== "connecting") return false;
|
|
1073
|
+
lastFrom = provider.name;
|
|
1074
|
+
lastError = toError(error);
|
|
1075
|
+
this.log.warn("provider failed to connect", {
|
|
1076
|
+
provider: provider.name,
|
|
1077
|
+
error: String(lastError)
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
this.fail(lastError ?? /* @__PURE__ */ new Error("provider connect failed"), "provider-failed");
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1042
1084
|
/** The outbound leg was answered (host's Twilio status callback). */
|
|
1043
1085
|
notifyAnswered() {
|
|
1044
1086
|
if (this.answered) return;
|
|
@@ -2159,6 +2201,9 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
2159
2201
|
const PREGREETING_MARK = "pre:greeting";
|
|
2160
2202
|
/** 400ms per frame — matches production burst-write implementations. */
|
|
2161
2203
|
const PREGREETING_CHUNK_BYTES = 3200;
|
|
2204
|
+
function toError(value) {
|
|
2205
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
2206
|
+
}
|
|
2162
2207
|
function safeJsonStringify(value) {
|
|
2163
2208
|
try {
|
|
2164
2209
|
return JSON.stringify(value) ?? "null";
|
|
@@ -2293,6 +2338,7 @@ var TwilioRealtimeBridge = class extends require_events.TypedEmitter {
|
|
|
2293
2338
|
transport,
|
|
2294
2339
|
start,
|
|
2295
2340
|
providerFactory: this.config.provider,
|
|
2341
|
+
fallbacks: this.config.fallbacks,
|
|
2296
2342
|
agent,
|
|
2297
2343
|
options,
|
|
2298
2344
|
store: this.store,
|
package/dist/index.d.cts
CHANGED
|
@@ -535,6 +535,19 @@ interface BridgeConfig {
|
|
|
535
535
|
/** The (root) agent, or a resolver for multi-tenant routing per call. */
|
|
536
536
|
agent: Agent | ((start: TwilioStartEvent) => Agent | Promise<Agent>);
|
|
537
537
|
provider: ProviderFactory;
|
|
538
|
+
/**
|
|
539
|
+
* Backup providers tried in order when `provider` fails to come up at call
|
|
540
|
+
* start (bad key, exhausted quota, outage, connect timeout). Each attempt
|
|
541
|
+
* emits `provider.fallback`; the call fails only when every fallback is
|
|
542
|
+
* exhausted. Connect-time only: once a provider answers, the call stays
|
|
543
|
+
* with it (mid-call reconnects keep using the same provider).
|
|
544
|
+
*
|
|
545
|
+
* ```ts
|
|
546
|
+
* provider: openaiRealtime(),
|
|
547
|
+
* fallbacks: [xaiRealtime(), geminiLive()],
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
fallbacks?: ProviderFactory[];
|
|
538
551
|
session?: Partial<SessionOptions> | ((start: TwilioStartEvent) => Partial<SessionOptions> | Promise<Partial<SessionOptions>>);
|
|
539
552
|
/** Twilio REST credentials — enables clean hangup, transfer, SMS/WhatsApp. */
|
|
540
553
|
twilio?: TwilioRestOptions;
|
|
@@ -593,6 +606,19 @@ interface ApprovalRequestInfo {
|
|
|
593
606
|
type VadSuggestionInfo = VadAdjustment & {
|
|
594
607
|
willAutoApply: boolean;
|
|
595
608
|
};
|
|
609
|
+
/**
|
|
610
|
+
* While ESTABLISHING the call, a provider failed to come up and the session
|
|
611
|
+
* is trying the next factory in the fallback chain. Fires only before the
|
|
612
|
+
* call is active — once a provider has connected, the call stays with it.
|
|
613
|
+
*/
|
|
614
|
+
interface ProviderFallbackInfo {
|
|
615
|
+
/** Name of the provider whose connect failed. */
|
|
616
|
+
from: string;
|
|
617
|
+
/** Name of the provider being tried instead. */
|
|
618
|
+
to: string;
|
|
619
|
+
/** The connect error that triggered the fallback. */
|
|
620
|
+
error: Error;
|
|
621
|
+
}
|
|
596
622
|
interface SessionEventMap {
|
|
597
623
|
'call.started': (info: CallStartedInfo) => void;
|
|
598
624
|
'call.ended': (info: {
|
|
@@ -611,6 +637,8 @@ interface SessionEventMap {
|
|
|
611
637
|
code?: number;
|
|
612
638
|
reason?: string;
|
|
613
639
|
}) => void;
|
|
640
|
+
/** Connect-time fallback: trying the next provider in the configured chain. */
|
|
641
|
+
'provider.fallback': (info: ProviderFallbackInfo) => void;
|
|
614
642
|
/** Generation-side: the model started/finished producing a response. */
|
|
615
643
|
'agent.speech.started': (info: {
|
|
616
644
|
responseId: string;
|
|
@@ -683,6 +711,8 @@ interface CallSessionDeps {
|
|
|
683
711
|
transport: TwilioMediaTransport;
|
|
684
712
|
start: TwilioStartEvent;
|
|
685
713
|
providerFactory: ProviderFactory;
|
|
714
|
+
/** Backup factories tried in order when `providerFactory` fails to connect. */
|
|
715
|
+
fallbacks?: readonly ProviderFactory[];
|
|
686
716
|
agent: Agent;
|
|
687
717
|
options: SessionOptions;
|
|
688
718
|
store: SessionStore;
|
|
@@ -713,6 +743,8 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
713
743
|
private readonly runningTools;
|
|
714
744
|
private readonly timers;
|
|
715
745
|
private provider;
|
|
746
|
+
/** Primary + fallbacks, in try-order. Only walked while connecting. */
|
|
747
|
+
private readonly providerChain;
|
|
716
748
|
private activeAgentValue;
|
|
717
749
|
private generating;
|
|
718
750
|
private currentResponseId;
|
|
@@ -772,6 +804,15 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
772
804
|
get transcript(): readonly TranscriptEntry[];
|
|
773
805
|
/** Connect the provider and activate the call. Called by the bridge. */
|
|
774
806
|
begin(): Promise<void>;
|
|
807
|
+
/**
|
|
808
|
+
* Walk primary + fallbacks until one connects. A provider that fails to
|
|
809
|
+
* come up is detached BEFORE the chain advances, so a half-open socket's
|
|
810
|
+
* late events can never reach the session once the next provider owns the
|
|
811
|
+
* call. Connect-time only by design: once a provider has answered, the
|
|
812
|
+
* call stays with it. Returns false after failing the call (chain
|
|
813
|
+
* exhausted) or when the call was torn down while connecting.
|
|
814
|
+
*/
|
|
815
|
+
private connectInitialProvider;
|
|
775
816
|
/** The outbound leg was answered (host's Twilio status callback). */
|
|
776
817
|
notifyAnswered(): void;
|
|
777
818
|
sendText(text: string, options?: {
|
|
@@ -1085,4 +1126,4 @@ declare class PlaybackTracker {
|
|
|
1085
1126
|
private maybeForget;
|
|
1086
1127
|
}
|
|
1087
1128
|
//#endregion
|
|
1088
|
-
export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, type Logger, type MarkEchoResult, NoiseAdaptiveVadController, type NoiseAdaptiveVadOptions, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type SessionUpdateOptions, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadAdjustment, type VadConfig, type VadNoiseMetrics, type VadSuggestionInfo, type VadTuningProfile, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
|
1129
|
+
export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, type Logger, type MarkEchoResult, NoiseAdaptiveVadController, type NoiseAdaptiveVadOptions, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderFallbackInfo, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type SessionUpdateOptions, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadAdjustment, type VadConfig, type VadNoiseMetrics, type VadSuggestionInfo, type VadTuningProfile, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
package/dist/index.d.mts
CHANGED
|
@@ -535,6 +535,19 @@ interface BridgeConfig {
|
|
|
535
535
|
/** The (root) agent, or a resolver for multi-tenant routing per call. */
|
|
536
536
|
agent: Agent | ((start: TwilioStartEvent) => Agent | Promise<Agent>);
|
|
537
537
|
provider: ProviderFactory;
|
|
538
|
+
/**
|
|
539
|
+
* Backup providers tried in order when `provider` fails to come up at call
|
|
540
|
+
* start (bad key, exhausted quota, outage, connect timeout). Each attempt
|
|
541
|
+
* emits `provider.fallback`; the call fails only when every fallback is
|
|
542
|
+
* exhausted. Connect-time only: once a provider answers, the call stays
|
|
543
|
+
* with it (mid-call reconnects keep using the same provider).
|
|
544
|
+
*
|
|
545
|
+
* ```ts
|
|
546
|
+
* provider: openaiRealtime(),
|
|
547
|
+
* fallbacks: [xaiRealtime(), geminiLive()],
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
fallbacks?: ProviderFactory[];
|
|
538
551
|
session?: Partial<SessionOptions> | ((start: TwilioStartEvent) => Partial<SessionOptions> | Promise<Partial<SessionOptions>>);
|
|
539
552
|
/** Twilio REST credentials — enables clean hangup, transfer, SMS/WhatsApp. */
|
|
540
553
|
twilio?: TwilioRestOptions;
|
|
@@ -593,6 +606,19 @@ interface ApprovalRequestInfo {
|
|
|
593
606
|
type VadSuggestionInfo = VadAdjustment & {
|
|
594
607
|
willAutoApply: boolean;
|
|
595
608
|
};
|
|
609
|
+
/**
|
|
610
|
+
* While ESTABLISHING the call, a provider failed to come up and the session
|
|
611
|
+
* is trying the next factory in the fallback chain. Fires only before the
|
|
612
|
+
* call is active — once a provider has connected, the call stays with it.
|
|
613
|
+
*/
|
|
614
|
+
interface ProviderFallbackInfo {
|
|
615
|
+
/** Name of the provider whose connect failed. */
|
|
616
|
+
from: string;
|
|
617
|
+
/** Name of the provider being tried instead. */
|
|
618
|
+
to: string;
|
|
619
|
+
/** The connect error that triggered the fallback. */
|
|
620
|
+
error: Error;
|
|
621
|
+
}
|
|
596
622
|
interface SessionEventMap {
|
|
597
623
|
'call.started': (info: CallStartedInfo) => void;
|
|
598
624
|
'call.ended': (info: {
|
|
@@ -611,6 +637,8 @@ interface SessionEventMap {
|
|
|
611
637
|
code?: number;
|
|
612
638
|
reason?: string;
|
|
613
639
|
}) => void;
|
|
640
|
+
/** Connect-time fallback: trying the next provider in the configured chain. */
|
|
641
|
+
'provider.fallback': (info: ProviderFallbackInfo) => void;
|
|
614
642
|
/** Generation-side: the model started/finished producing a response. */
|
|
615
643
|
'agent.speech.started': (info: {
|
|
616
644
|
responseId: string;
|
|
@@ -683,6 +711,8 @@ interface CallSessionDeps {
|
|
|
683
711
|
transport: TwilioMediaTransport;
|
|
684
712
|
start: TwilioStartEvent;
|
|
685
713
|
providerFactory: ProviderFactory;
|
|
714
|
+
/** Backup factories tried in order when `providerFactory` fails to connect. */
|
|
715
|
+
fallbacks?: readonly ProviderFactory[];
|
|
686
716
|
agent: Agent;
|
|
687
717
|
options: SessionOptions;
|
|
688
718
|
store: SessionStore;
|
|
@@ -713,6 +743,8 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
713
743
|
private readonly runningTools;
|
|
714
744
|
private readonly timers;
|
|
715
745
|
private provider;
|
|
746
|
+
/** Primary + fallbacks, in try-order. Only walked while connecting. */
|
|
747
|
+
private readonly providerChain;
|
|
716
748
|
private activeAgentValue;
|
|
717
749
|
private generating;
|
|
718
750
|
private currentResponseId;
|
|
@@ -772,6 +804,15 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
772
804
|
get transcript(): readonly TranscriptEntry[];
|
|
773
805
|
/** Connect the provider and activate the call. Called by the bridge. */
|
|
774
806
|
begin(): Promise<void>;
|
|
807
|
+
/**
|
|
808
|
+
* Walk primary + fallbacks until one connects. A provider that fails to
|
|
809
|
+
* come up is detached BEFORE the chain advances, so a half-open socket's
|
|
810
|
+
* late events can never reach the session once the next provider owns the
|
|
811
|
+
* call. Connect-time only by design: once a provider has answered, the
|
|
812
|
+
* call stays with it. Returns false after failing the call (chain
|
|
813
|
+
* exhausted) or when the call was torn down while connecting.
|
|
814
|
+
*/
|
|
815
|
+
private connectInitialProvider;
|
|
775
816
|
/** The outbound leg was answered (host's Twilio status callback). */
|
|
776
817
|
notifyAnswered(): void;
|
|
777
818
|
sendText(text: string, options?: {
|
|
@@ -1085,4 +1126,4 @@ declare class PlaybackTracker {
|
|
|
1085
1126
|
private maybeForget;
|
|
1086
1127
|
}
|
|
1087
1128
|
//#endregion
|
|
1088
|
-
export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, type Logger, type MarkEchoResult, NoiseAdaptiveVadController, type NoiseAdaptiveVadOptions, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type SessionUpdateOptions, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadAdjustment, type VadConfig, type VadNoiseMetrics, type VadSuggestionInfo, type VadTuningProfile, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
|
1129
|
+
export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, type Logger, type MarkEchoResult, NoiseAdaptiveVadController, type NoiseAdaptiveVadOptions, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderFallbackInfo, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type SessionUpdateOptions, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadAdjustment, type VadConfig, type VadNoiseMetrics, type VadSuggestionInfo, type VadTuningProfile, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
package/dist/index.mjs
CHANGED
|
@@ -903,6 +903,8 @@ var CallSession = class extends TypedEmitter {
|
|
|
903
903
|
runningTools = /* @__PURE__ */ new Map();
|
|
904
904
|
timers = /* @__PURE__ */ new Set();
|
|
905
905
|
provider = null;
|
|
906
|
+
/** Primary + fallbacks, in try-order. Only walked while connecting. */
|
|
907
|
+
providerChain;
|
|
906
908
|
activeAgentValue;
|
|
907
909
|
generating = false;
|
|
908
910
|
currentResponseId = null;
|
|
@@ -961,6 +963,7 @@ var CallSession = class extends TypedEmitter {
|
|
|
961
963
|
this.callSid = deps.start.start.callSid;
|
|
962
964
|
this.streamSid = deps.start.start.streamSid ?? deps.start.streamSid;
|
|
963
965
|
this.log = childLogger(deps.logger, { callSid: this.callSid });
|
|
966
|
+
this.providerChain = [deps.providerFactory, ...deps.fallbacks ?? []];
|
|
964
967
|
this.activeAgentValue = deps.agent;
|
|
965
968
|
this.context = new SessionContext(deps.options.context);
|
|
966
969
|
this.interruptions = new InterruptionController(deps.options.interruptions);
|
|
@@ -999,17 +1002,7 @@ var CallSession = class extends TypedEmitter {
|
|
|
999
1002
|
/** Connect the provider and activate the call. Called by the bridge. */
|
|
1000
1003
|
async begin() {
|
|
1001
1004
|
this.playPreGreeting();
|
|
1002
|
-
|
|
1003
|
-
this.provider = this.deps.providerFactory({
|
|
1004
|
-
logger: this.log,
|
|
1005
|
-
callSid: this.callSid
|
|
1006
|
-
});
|
|
1007
|
-
this.wireProvider(this.provider);
|
|
1008
|
-
await this.provider.connect(this.buildProviderInit());
|
|
1009
|
-
} catch (error) {
|
|
1010
|
-
this.fail(error instanceof Error ? error : new Error(String(error)), "provider-failed");
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1005
|
+
if (!await this.connectInitialProvider()) return;
|
|
1013
1006
|
if (this.stateValue !== "connecting") return;
|
|
1014
1007
|
this.stateValue = "active";
|
|
1015
1008
|
this.emit("provider.connected");
|
|
@@ -1035,6 +1028,55 @@ var CallSession = class extends TypedEmitter {
|
|
|
1035
1028
|
this.saveSnapshot();
|
|
1036
1029
|
this.maybeGreet();
|
|
1037
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Walk primary + fallbacks until one connects. A provider that fails to
|
|
1033
|
+
* come up is detached BEFORE the chain advances, so a half-open socket's
|
|
1034
|
+
* late events can never reach the session once the next provider owns the
|
|
1035
|
+
* call. Connect-time only by design: once a provider has answered, the
|
|
1036
|
+
* call stays with it. Returns false after failing the call (chain
|
|
1037
|
+
* exhausted) or when the call was torn down while connecting.
|
|
1038
|
+
*/
|
|
1039
|
+
async connectInitialProvider() {
|
|
1040
|
+
let lastFrom = "provider";
|
|
1041
|
+
let lastError = null;
|
|
1042
|
+
for (const factory of this.providerChain) {
|
|
1043
|
+
let provider;
|
|
1044
|
+
try {
|
|
1045
|
+
provider = factory({
|
|
1046
|
+
logger: this.log,
|
|
1047
|
+
callSid: this.callSid
|
|
1048
|
+
});
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
lastError = toError(error);
|
|
1051
|
+
this.log.warn("provider factory threw — trying the next fallback", { error: String(lastError) });
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (lastError) this.emit("provider.fallback", {
|
|
1055
|
+
from: lastFrom,
|
|
1056
|
+
to: provider.name,
|
|
1057
|
+
error: lastError
|
|
1058
|
+
});
|
|
1059
|
+
this.provider = provider;
|
|
1060
|
+
this.wireProvider(provider);
|
|
1061
|
+
try {
|
|
1062
|
+
await provider.connect(this.buildProviderInit());
|
|
1063
|
+
return true;
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
provider.removeAllListeners();
|
|
1066
|
+
provider.close().catch(() => {});
|
|
1067
|
+
this.provider = null;
|
|
1068
|
+
if (this.stateValue !== "connecting") return false;
|
|
1069
|
+
lastFrom = provider.name;
|
|
1070
|
+
lastError = toError(error);
|
|
1071
|
+
this.log.warn("provider failed to connect", {
|
|
1072
|
+
provider: provider.name,
|
|
1073
|
+
error: String(lastError)
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
this.fail(lastError ?? /* @__PURE__ */ new Error("provider connect failed"), "provider-failed");
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1038
1080
|
/** The outbound leg was answered (host's Twilio status callback). */
|
|
1039
1081
|
notifyAnswered() {
|
|
1040
1082
|
if (this.answered) return;
|
|
@@ -2155,6 +2197,9 @@ var CallSession = class extends TypedEmitter {
|
|
|
2155
2197
|
const PREGREETING_MARK = "pre:greeting";
|
|
2156
2198
|
/** 400ms per frame — matches production burst-write implementations. */
|
|
2157
2199
|
const PREGREETING_CHUNK_BYTES = 3200;
|
|
2200
|
+
function toError(value) {
|
|
2201
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
2202
|
+
}
|
|
2158
2203
|
function safeJsonStringify(value) {
|
|
2159
2204
|
try {
|
|
2160
2205
|
return JSON.stringify(value) ?? "null";
|
|
@@ -2289,6 +2334,7 @@ var TwilioRealtimeBridge = class extends TypedEmitter {
|
|
|
2289
2334
|
transport,
|
|
2290
2335
|
start,
|
|
2291
2336
|
providerFactory: this.config.provider,
|
|
2337
|
+
fallbacks: this.config.fallbacks,
|
|
2292
2338
|
agent,
|
|
2293
2339
|
options,
|
|
2294
2340
|
store: this.store,
|
package/dist/openai.cjs
CHANGED
|
@@ -30,12 +30,16 @@ const OPENAI_REALTIME_VOICES = [
|
|
|
30
30
|
"shimmer",
|
|
31
31
|
"verse"
|
|
32
32
|
];
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Create a provider factory for the bridge (`provider: openaiRealtime({...})`).
|
|
35
|
+
*
|
|
36
|
+
* Credentials are resolved per call, when the factory runs — not while the
|
|
37
|
+
* config is being built. A missing key therefore fails THAT provider's
|
|
38
|
+
* connect (with a clear error) instead of crashing config construction, so a
|
|
39
|
+
* fallback chain (`BridgeConfig.fallbacks`) can absorb it.
|
|
40
|
+
*/
|
|
34
41
|
function openaiRealtime(options = {}) {
|
|
35
|
-
const apiKey = require_env.resolveApiKey(options.apiKey, OPENAI_KEY_ENV_VARS);
|
|
36
|
-
if (!apiKey) throw new Error(`openaiRealtime: apiKey missing (pass apiKey or set one of ${OPENAI_KEY_ENV_VARS.join("/")})`);
|
|
37
42
|
const config = {
|
|
38
|
-
apiKey,
|
|
39
43
|
model: options.model ?? "gpt-realtime",
|
|
40
44
|
voice: options.voice ?? "marin",
|
|
41
45
|
baseUrl: options.baseUrl,
|
|
@@ -52,7 +56,12 @@ function openaiRealtime(options = {}) {
|
|
|
52
56
|
} }
|
|
53
57
|
};
|
|
54
58
|
return ({ logger }) => {
|
|
55
|
-
|
|
59
|
+
const apiKey = require_env.resolveApiKey(options.apiKey, OPENAI_KEY_ENV_VARS);
|
|
60
|
+
if (!apiKey) throw new Error(`openaiRealtime: apiKey missing (pass apiKey or set one of ${OPENAI_KEY_ENV_VARS.join("/")})`);
|
|
61
|
+
return new require_OpenAICompatibleProvider.OpenAICompatibleProvider({
|
|
62
|
+
...config,
|
|
63
|
+
apiKey
|
|
64
|
+
}, logger);
|
|
56
65
|
};
|
|
57
66
|
}
|
|
58
67
|
//#endregion
|
package/dist/openai.d.cts
CHANGED
|
@@ -136,7 +136,14 @@ interface OpenAIRealtimeOptions {
|
|
|
136
136
|
sessionOptions?: Record<string, unknown>;
|
|
137
137
|
connectTimeoutMs?: number;
|
|
138
138
|
}
|
|
139
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Create a provider factory for the bridge (`provider: openaiRealtime({...})`).
|
|
141
|
+
*
|
|
142
|
+
* Credentials are resolved per call, when the factory runs — not while the
|
|
143
|
+
* config is being built. A missing key therefore fails THAT provider's
|
|
144
|
+
* connect (with a clear error) instead of crashing config construction, so a
|
|
145
|
+
* fallback chain (`BridgeConfig.fallbacks`) can absorb it.
|
|
146
|
+
*/
|
|
140
147
|
declare function openaiRealtime(options?: OpenAIRealtimeOptions): ProviderFactory;
|
|
141
148
|
//#endregion
|
|
142
149
|
export { OPENAI_DEFAULT_MODEL, OPENAI_DEFAULT_VOICE, OPENAI_KEY_ENV_VARS, OPENAI_REALTIME_VOICES, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, OpenAIRealtimeOptions, OpenAIRealtimeVoice, buildSessionUpdate, buildTurnDetection, openaiRealtime };
|
package/dist/openai.d.mts
CHANGED
|
@@ -136,7 +136,14 @@ interface OpenAIRealtimeOptions {
|
|
|
136
136
|
sessionOptions?: Record<string, unknown>;
|
|
137
137
|
connectTimeoutMs?: number;
|
|
138
138
|
}
|
|
139
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Create a provider factory for the bridge (`provider: openaiRealtime({...})`).
|
|
141
|
+
*
|
|
142
|
+
* Credentials are resolved per call, when the factory runs — not while the
|
|
143
|
+
* config is being built. A missing key therefore fails THAT provider's
|
|
144
|
+
* connect (with a clear error) instead of crashing config construction, so a
|
|
145
|
+
* fallback chain (`BridgeConfig.fallbacks`) can absorb it.
|
|
146
|
+
*/
|
|
140
147
|
declare function openaiRealtime(options?: OpenAIRealtimeOptions): ProviderFactory;
|
|
141
148
|
//#endregion
|
|
142
149
|
export { OPENAI_DEFAULT_MODEL, OPENAI_DEFAULT_VOICE, OPENAI_KEY_ENV_VARS, OPENAI_REALTIME_VOICES, OpenAICompatibleProvider, type OpenAICompatibleProviderConfig, OpenAIRealtimeOptions, OpenAIRealtimeVoice, buildSessionUpdate, buildTurnDetection, openaiRealtime };
|
package/dist/openai.mjs
CHANGED
|
@@ -29,12 +29,16 @@ const OPENAI_REALTIME_VOICES = [
|
|
|
29
29
|
"shimmer",
|
|
30
30
|
"verse"
|
|
31
31
|
];
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Create a provider factory for the bridge (`provider: openaiRealtime({...})`).
|
|
34
|
+
*
|
|
35
|
+
* Credentials are resolved per call, when the factory runs — not while the
|
|
36
|
+
* config is being built. A missing key therefore fails THAT provider's
|
|
37
|
+
* connect (with a clear error) instead of crashing config construction, so a
|
|
38
|
+
* fallback chain (`BridgeConfig.fallbacks`) can absorb it.
|
|
39
|
+
*/
|
|
33
40
|
function openaiRealtime(options = {}) {
|
|
34
|
-
const apiKey = resolveApiKey(options.apiKey, OPENAI_KEY_ENV_VARS);
|
|
35
|
-
if (!apiKey) throw new Error(`openaiRealtime: apiKey missing (pass apiKey or set one of ${OPENAI_KEY_ENV_VARS.join("/")})`);
|
|
36
41
|
const config = {
|
|
37
|
-
apiKey,
|
|
38
42
|
model: options.model ?? "gpt-realtime",
|
|
39
43
|
voice: options.voice ?? "marin",
|
|
40
44
|
baseUrl: options.baseUrl,
|
|
@@ -51,7 +55,12 @@ function openaiRealtime(options = {}) {
|
|
|
51
55
|
} }
|
|
52
56
|
};
|
|
53
57
|
return ({ logger }) => {
|
|
54
|
-
|
|
58
|
+
const apiKey = resolveApiKey(options.apiKey, OPENAI_KEY_ENV_VARS);
|
|
59
|
+
if (!apiKey) throw new Error(`openaiRealtime: apiKey missing (pass apiKey or set one of ${OPENAI_KEY_ENV_VARS.join("/")})`);
|
|
60
|
+
return new OpenAICompatibleProvider({
|
|
61
|
+
...config,
|
|
62
|
+
apiKey
|
|
63
|
+
}, logger);
|
|
55
64
|
};
|
|
56
65
|
}
|
|
57
66
|
//#endregion
|
package/dist/testing.cjs
CHANGED
|
@@ -358,6 +358,14 @@ var FakeOpenAIConnection = class {
|
|
|
358
358
|
};
|
|
359
359
|
var FakeOpenAIServer = class FakeOpenAIServer {
|
|
360
360
|
connections = [];
|
|
361
|
+
/** Flip at runtime to start/stop refusing sessions (see the option's docs). */
|
|
362
|
+
refuseConnections;
|
|
363
|
+
/** Sockets closed by `refuseConnections` before a session was created. */
|
|
364
|
+
refusedConnections = 0;
|
|
365
|
+
/** Set at runtime to start rejecting HTTP upgrades; null re-accepts. */
|
|
366
|
+
rejectUpgrade;
|
|
367
|
+
/** Upgrades rejected by `rejectUpgrade`. */
|
|
368
|
+
rejectedUpgrades = 0;
|
|
361
369
|
wss;
|
|
362
370
|
options;
|
|
363
371
|
url;
|
|
@@ -365,7 +373,14 @@ var FakeOpenAIServer = class FakeOpenAIServer {
|
|
|
365
373
|
this.wss = wss;
|
|
366
374
|
this.url = url;
|
|
367
375
|
this.options = options;
|
|
376
|
+
this.refuseConnections = options.refuseConnections ?? false;
|
|
377
|
+
this.rejectUpgrade = options.rejectUpgrade ?? null;
|
|
368
378
|
wss.on("connection", (socket, request) => {
|
|
379
|
+
if (this.refuseConnections) {
|
|
380
|
+
this.refusedConnections++;
|
|
381
|
+
socket.close(1011, "fake server refusing sessions");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
369
384
|
const connection = new FakeOpenAIConnection(this, socket, request.headers);
|
|
370
385
|
this.connections.push(connection);
|
|
371
386
|
socket.on("message", (raw) => {
|
|
@@ -388,9 +403,19 @@ var FakeOpenAIServer = class FakeOpenAIServer {
|
|
|
388
403
|
});
|
|
389
404
|
}
|
|
390
405
|
static async start(options = {}) {
|
|
406
|
+
const holder = {};
|
|
391
407
|
const wss = new ws.WebSocketServer({
|
|
392
408
|
port: 0,
|
|
393
|
-
host: "127.0.0.1"
|
|
409
|
+
host: "127.0.0.1",
|
|
410
|
+
verifyClient: (_info, done) => {
|
|
411
|
+
const reject = holder.server?.rejectUpgrade;
|
|
412
|
+
if (reject) {
|
|
413
|
+
holder.server.rejectedUpgrades++;
|
|
414
|
+
done(false, reject.status, reject.body ?? "rejected by fake server");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
done(true);
|
|
418
|
+
}
|
|
394
419
|
});
|
|
395
420
|
await new Promise((resolve, reject) => {
|
|
396
421
|
wss.once("listening", resolve);
|
|
@@ -398,7 +423,9 @@ var FakeOpenAIServer = class FakeOpenAIServer {
|
|
|
398
423
|
});
|
|
399
424
|
const address = wss.address();
|
|
400
425
|
if (typeof address === "string" || address === null) throw new Error("no server address");
|
|
401
|
-
|
|
426
|
+
const server = new FakeOpenAIServer(wss, `ws://127.0.0.1:${address.port}`, options);
|
|
427
|
+
holder.server = server;
|
|
428
|
+
return server;
|
|
402
429
|
}
|
|
403
430
|
get latest() {
|
|
404
431
|
const connection = this.connections[this.connections.length - 1];
|
package/dist/testing.d.cts
CHANGED
|
@@ -76,6 +76,25 @@ declare class FakeTwilioMediaStream implements WebSocketLike {
|
|
|
76
76
|
interface FakeOpenAIServerOptions {
|
|
77
77
|
/** Auto-ack session.update with session.updated. Default true. */
|
|
78
78
|
autoAckSessionUpdate?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Refuse sessions: close each new socket immediately, before
|
|
81
|
+
* session.created — a provider that is down or rejecting (exercises
|
|
82
|
+
* connect failures and provider fallback chains). Mutable at runtime via
|
|
83
|
+
* the server's `refuseConnections` field; refused sockets are counted in
|
|
84
|
+
* `refusedConnections` and never appear in `connections`.
|
|
85
|
+
*/
|
|
86
|
+
refuseConnections?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Reject the HTTP upgrade itself with this status + body — how real
|
|
89
|
+
* providers surface an invalid/expired key (401), exhausted credits
|
|
90
|
+
* (403/429), or an internal error (5xx). Mutable at runtime via the
|
|
91
|
+
* server's `rejectUpgrade` field (null re-accepts); rejections are counted
|
|
92
|
+
* in `rejectedUpgrades` and never appear in `connections`.
|
|
93
|
+
*/
|
|
94
|
+
rejectUpgrade?: {
|
|
95
|
+
status: number;
|
|
96
|
+
body?: string;
|
|
97
|
+
};
|
|
79
98
|
}
|
|
80
99
|
interface FakeAudioResponseOptions {
|
|
81
100
|
responseId?: string;
|
|
@@ -119,6 +138,17 @@ declare class FakeOpenAIConnection {
|
|
|
119
138
|
}
|
|
120
139
|
declare class FakeOpenAIServer {
|
|
121
140
|
readonly connections: FakeOpenAIConnection[];
|
|
141
|
+
/** Flip at runtime to start/stop refusing sessions (see the option's docs). */
|
|
142
|
+
refuseConnections: boolean;
|
|
143
|
+
/** Sockets closed by `refuseConnections` before a session was created. */
|
|
144
|
+
refusedConnections: number;
|
|
145
|
+
/** Set at runtime to start rejecting HTTP upgrades; null re-accepts. */
|
|
146
|
+
rejectUpgrade: {
|
|
147
|
+
status: number;
|
|
148
|
+
body?: string;
|
|
149
|
+
} | null;
|
|
150
|
+
/** Upgrades rejected by `rejectUpgrade`. */
|
|
151
|
+
rejectedUpgrades: number;
|
|
122
152
|
private readonly wss;
|
|
123
153
|
private readonly options;
|
|
124
154
|
readonly url: string;
|
package/dist/testing.d.mts
CHANGED
|
@@ -76,6 +76,25 @@ declare class FakeTwilioMediaStream implements WebSocketLike {
|
|
|
76
76
|
interface FakeOpenAIServerOptions {
|
|
77
77
|
/** Auto-ack session.update with session.updated. Default true. */
|
|
78
78
|
autoAckSessionUpdate?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Refuse sessions: close each new socket immediately, before
|
|
81
|
+
* session.created — a provider that is down or rejecting (exercises
|
|
82
|
+
* connect failures and provider fallback chains). Mutable at runtime via
|
|
83
|
+
* the server's `refuseConnections` field; refused sockets are counted in
|
|
84
|
+
* `refusedConnections` and never appear in `connections`.
|
|
85
|
+
*/
|
|
86
|
+
refuseConnections?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Reject the HTTP upgrade itself with this status + body — how real
|
|
89
|
+
* providers surface an invalid/expired key (401), exhausted credits
|
|
90
|
+
* (403/429), or an internal error (5xx). Mutable at runtime via the
|
|
91
|
+
* server's `rejectUpgrade` field (null re-accepts); rejections are counted
|
|
92
|
+
* in `rejectedUpgrades` and never appear in `connections`.
|
|
93
|
+
*/
|
|
94
|
+
rejectUpgrade?: {
|
|
95
|
+
status: number;
|
|
96
|
+
body?: string;
|
|
97
|
+
};
|
|
79
98
|
}
|
|
80
99
|
interface FakeAudioResponseOptions {
|
|
81
100
|
responseId?: string;
|
|
@@ -119,6 +138,17 @@ declare class FakeOpenAIConnection {
|
|
|
119
138
|
}
|
|
120
139
|
declare class FakeOpenAIServer {
|
|
121
140
|
readonly connections: FakeOpenAIConnection[];
|
|
141
|
+
/** Flip at runtime to start/stop refusing sessions (see the option's docs). */
|
|
142
|
+
refuseConnections: boolean;
|
|
143
|
+
/** Sockets closed by `refuseConnections` before a session was created. */
|
|
144
|
+
refusedConnections: number;
|
|
145
|
+
/** Set at runtime to start rejecting HTTP upgrades; null re-accepts. */
|
|
146
|
+
rejectUpgrade: {
|
|
147
|
+
status: number;
|
|
148
|
+
body?: string;
|
|
149
|
+
} | null;
|
|
150
|
+
/** Upgrades rejected by `rejectUpgrade`. */
|
|
151
|
+
rejectedUpgrades: number;
|
|
122
152
|
private readonly wss;
|
|
123
153
|
private readonly options;
|
|
124
154
|
readonly url: string;
|
package/dist/testing.mjs
CHANGED
|
@@ -357,6 +357,14 @@ var FakeOpenAIConnection = class {
|
|
|
357
357
|
};
|
|
358
358
|
var FakeOpenAIServer = class FakeOpenAIServer {
|
|
359
359
|
connections = [];
|
|
360
|
+
/** Flip at runtime to start/stop refusing sessions (see the option's docs). */
|
|
361
|
+
refuseConnections;
|
|
362
|
+
/** Sockets closed by `refuseConnections` before a session was created. */
|
|
363
|
+
refusedConnections = 0;
|
|
364
|
+
/** Set at runtime to start rejecting HTTP upgrades; null re-accepts. */
|
|
365
|
+
rejectUpgrade;
|
|
366
|
+
/** Upgrades rejected by `rejectUpgrade`. */
|
|
367
|
+
rejectedUpgrades = 0;
|
|
360
368
|
wss;
|
|
361
369
|
options;
|
|
362
370
|
url;
|
|
@@ -364,7 +372,14 @@ var FakeOpenAIServer = class FakeOpenAIServer {
|
|
|
364
372
|
this.wss = wss;
|
|
365
373
|
this.url = url;
|
|
366
374
|
this.options = options;
|
|
375
|
+
this.refuseConnections = options.refuseConnections ?? false;
|
|
376
|
+
this.rejectUpgrade = options.rejectUpgrade ?? null;
|
|
367
377
|
wss.on("connection", (socket, request) => {
|
|
378
|
+
if (this.refuseConnections) {
|
|
379
|
+
this.refusedConnections++;
|
|
380
|
+
socket.close(1011, "fake server refusing sessions");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
368
383
|
const connection = new FakeOpenAIConnection(this, socket, request.headers);
|
|
369
384
|
this.connections.push(connection);
|
|
370
385
|
socket.on("message", (raw) => {
|
|
@@ -387,9 +402,19 @@ var FakeOpenAIServer = class FakeOpenAIServer {
|
|
|
387
402
|
});
|
|
388
403
|
}
|
|
389
404
|
static async start(options = {}) {
|
|
405
|
+
const holder = {};
|
|
390
406
|
const wss = new WebSocketServer({
|
|
391
407
|
port: 0,
|
|
392
|
-
host: "127.0.0.1"
|
|
408
|
+
host: "127.0.0.1",
|
|
409
|
+
verifyClient: (_info, done) => {
|
|
410
|
+
const reject = holder.server?.rejectUpgrade;
|
|
411
|
+
if (reject) {
|
|
412
|
+
holder.server.rejectedUpgrades++;
|
|
413
|
+
done(false, reject.status, reject.body ?? "rejected by fake server");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
done(true);
|
|
417
|
+
}
|
|
393
418
|
});
|
|
394
419
|
await new Promise((resolve, reject) => {
|
|
395
420
|
wss.once("listening", resolve);
|
|
@@ -397,7 +422,9 @@ var FakeOpenAIServer = class FakeOpenAIServer {
|
|
|
397
422
|
});
|
|
398
423
|
const address = wss.address();
|
|
399
424
|
if (typeof address === "string" || address === null) throw new Error("no server address");
|
|
400
|
-
|
|
425
|
+
const server = new FakeOpenAIServer(wss, `ws://127.0.0.1:${address.port}`, options);
|
|
426
|
+
holder.server = server;
|
|
427
|
+
return server;
|
|
401
428
|
}
|
|
402
429
|
get latest() {
|
|
403
430
|
const connection = this.connections[this.connections.length - 1];
|
package/dist/xai.cjs
CHANGED
|
@@ -59,13 +59,15 @@ function buildXaiSessionUpdate(init, config = {}) {
|
|
|
59
59
|
session
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Create an xAI Grok Voice provider factory for the bridge. Credentials are
|
|
64
|
+
* resolved per call (see `openaiRealtime` — a missing key fails that call's
|
|
65
|
+
* connect so a fallback chain can absorb it, instead of crashing config
|
|
66
|
+
* construction).
|
|
67
|
+
*/
|
|
63
68
|
function xaiRealtime(options = {}) {
|
|
64
|
-
const apiKey = require_env.resolveApiKey(options.apiKey, XAI_KEY_ENV_VARS);
|
|
65
|
-
if (!apiKey) throw new Error(`xaiRealtime: apiKey missing (pass apiKey or set one of ${XAI_KEY_ENV_VARS.join("/")})`);
|
|
66
69
|
const transcription = options.transcription === false ? false : options.transcription ? { language: options.transcription.languageHint } : void 0;
|
|
67
70
|
const config = {
|
|
68
|
-
apiKey,
|
|
69
71
|
model: options.model ?? "grok-voice-latest",
|
|
70
72
|
voice: options.voice ?? "eve",
|
|
71
73
|
baseUrl: options.baseUrl ?? "wss://api.x.ai/v1/realtime",
|
|
@@ -86,7 +88,14 @@ function xaiRealtime(options = {}) {
|
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
};
|
|
89
|
-
return ({ logger }) =>
|
|
91
|
+
return ({ logger }) => {
|
|
92
|
+
const apiKey = require_env.resolveApiKey(options.apiKey, XAI_KEY_ENV_VARS);
|
|
93
|
+
if (!apiKey) throw new Error(`xaiRealtime: apiKey missing (pass apiKey or set one of ${XAI_KEY_ENV_VARS.join("/")})`);
|
|
94
|
+
return new require_OpenAICompatibleProvider.OpenAICompatibleProvider({
|
|
95
|
+
...config,
|
|
96
|
+
apiKey
|
|
97
|
+
}, logger);
|
|
98
|
+
};
|
|
90
99
|
}
|
|
91
100
|
//#endregion
|
|
92
101
|
exports.XAI_BASE_URL = XAI_BASE_URL;
|
package/dist/xai.d.cts
CHANGED
|
@@ -29,7 +29,12 @@ declare function buildXaiSessionUpdate(init: ProviderSessionInit, config?: {
|
|
|
29
29
|
defaultVoice?: string;
|
|
30
30
|
extraSessionOptions?: Record<string, unknown>;
|
|
31
31
|
}): Record<string, unknown>;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Create an xAI Grok Voice provider factory for the bridge. Credentials are
|
|
34
|
+
* resolved per call (see `openaiRealtime` — a missing key fails that call's
|
|
35
|
+
* connect so a fallback chain can absorb it, instead of crashing config
|
|
36
|
+
* construction).
|
|
37
|
+
*/
|
|
33
38
|
declare function xaiRealtime(options?: XaiRealtimeOptions): ProviderFactory;
|
|
34
39
|
//#endregion
|
|
35
40
|
export { XAI_BASE_URL, XAI_DEFAULT_MODEL, XAI_DEFAULT_VOICE, XAI_KEY_ENV_VARS, XaiRealtimeOptions, buildXaiSessionUpdate, xaiRealtime };
|
package/dist/xai.d.mts
CHANGED
|
@@ -29,7 +29,12 @@ declare function buildXaiSessionUpdate(init: ProviderSessionInit, config?: {
|
|
|
29
29
|
defaultVoice?: string;
|
|
30
30
|
extraSessionOptions?: Record<string, unknown>;
|
|
31
31
|
}): Record<string, unknown>;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Create an xAI Grok Voice provider factory for the bridge. Credentials are
|
|
34
|
+
* resolved per call (see `openaiRealtime` — a missing key fails that call's
|
|
35
|
+
* connect so a fallback chain can absorb it, instead of crashing config
|
|
36
|
+
* construction).
|
|
37
|
+
*/
|
|
33
38
|
declare function xaiRealtime(options?: XaiRealtimeOptions): ProviderFactory;
|
|
34
39
|
//#endregion
|
|
35
40
|
export { XAI_BASE_URL, XAI_DEFAULT_MODEL, XAI_DEFAULT_VOICE, XAI_KEY_ENV_VARS, XaiRealtimeOptions, buildXaiSessionUpdate, xaiRealtime };
|
package/dist/xai.mjs
CHANGED
|
@@ -58,13 +58,15 @@ function buildXaiSessionUpdate(init, config = {}) {
|
|
|
58
58
|
session
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Create an xAI Grok Voice provider factory for the bridge. Credentials are
|
|
63
|
+
* resolved per call (see `openaiRealtime` — a missing key fails that call's
|
|
64
|
+
* connect so a fallback chain can absorb it, instead of crashing config
|
|
65
|
+
* construction).
|
|
66
|
+
*/
|
|
62
67
|
function xaiRealtime(options = {}) {
|
|
63
|
-
const apiKey = resolveApiKey(options.apiKey, XAI_KEY_ENV_VARS);
|
|
64
|
-
if (!apiKey) throw new Error(`xaiRealtime: apiKey missing (pass apiKey or set one of ${XAI_KEY_ENV_VARS.join("/")})`);
|
|
65
68
|
const transcription = options.transcription === false ? false : options.transcription ? { language: options.transcription.languageHint } : void 0;
|
|
66
69
|
const config = {
|
|
67
|
-
apiKey,
|
|
68
70
|
model: options.model ?? "grok-voice-latest",
|
|
69
71
|
voice: options.voice ?? "eve",
|
|
70
72
|
baseUrl: options.baseUrl ?? "wss://api.x.ai/v1/realtime",
|
|
@@ -85,7 +87,14 @@ function xaiRealtime(options = {}) {
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
};
|
|
88
|
-
return ({ logger }) =>
|
|
90
|
+
return ({ logger }) => {
|
|
91
|
+
const apiKey = resolveApiKey(options.apiKey, XAI_KEY_ENV_VARS);
|
|
92
|
+
if (!apiKey) throw new Error(`xaiRealtime: apiKey missing (pass apiKey or set one of ${XAI_KEY_ENV_VARS.join("/")})`);
|
|
93
|
+
return new OpenAICompatibleProvider({
|
|
94
|
+
...config,
|
|
95
|
+
apiKey
|
|
96
|
+
}, logger);
|
|
97
|
+
};
|
|
89
98
|
}
|
|
90
99
|
//#endregion
|
|
91
100
|
export { XAI_BASE_URL, XAI_DEFAULT_MODEL, XAI_DEFAULT_VOICE, XAI_KEY_ENV_VARS, buildXaiSessionUpdate, xaiRealtime };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtime-voice-agents",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Provider-agnostic bridge between Twilio Media Streams and realtime speech-to-speech AI APIs (OpenAI Realtime, xAI Grok Voice, Gemini Live). Multi-agent handoffs, Zod tools with execution strategies, mark-based playback tracking, interruption guards, and hold audio — for Node.js voice agents over the phone.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twilio",
|