@absolutejs/voice 0.0.22-beta.31 → 0.0.22-beta.33
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/dist/index.d.ts +2 -0
- package/dist/index.js +336 -6
- package/dist/modelAdapters.d.ts +6 -0
- package/dist/providerAdapters.d.ts +33 -0
- package/dist/providerHealth.d.ts +1 -0
- package/dist/testing/index.js +42 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap
|
|
|
7
7
|
export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
|
|
8
8
|
export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
|
|
9
9
|
export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
|
|
10
|
+
export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
|
|
10
11
|
export { buildVoiceTraceReplay, createVoiceMemoryTraceSinkDeliveryStore, createVoiceTraceHTTPSink, createVoiceMemoryTraceEventStore, createVoiceTraceSinkDeliveryId, createVoiceTraceSinkDeliveryRecord, createVoiceTraceSinkStore, createVoiceTraceEvent, createVoiceTraceEventId, deliverVoiceTraceEventsToSinks, evaluateVoiceTrace, exportVoiceTrace, filterVoiceTraceEvents, pruneVoiceTraceEvents, redactVoiceTraceEvent, redactVoiceTraceEvents, redactVoiceTraceText, renderVoiceTraceHTML, renderVoiceTraceMarkdown, resolveVoiceTraceRedactionOptions, selectVoiceTraceEventsForPrune, summarizeVoiceTrace } from './trace';
|
|
11
12
|
export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
|
|
12
13
|
export { createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore } from './postgresStore';
|
|
@@ -36,6 +37,7 @@ export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssi
|
|
|
36
37
|
export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
|
|
37
38
|
export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
|
|
38
39
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
|
40
|
+
export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
|
|
39
41
|
export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
|
|
40
42
|
export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
|
|
41
43
|
export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
|
package/dist/index.js
CHANGED
|
@@ -6552,7 +6552,8 @@ var summarizeVoiceProviderHealth = async (input) => {
|
|
|
6552
6552
|
rateLimited: false,
|
|
6553
6553
|
recommended: false,
|
|
6554
6554
|
runCount: 0,
|
|
6555
|
-
status: "idle"
|
|
6555
|
+
status: "idle",
|
|
6556
|
+
timeoutCount: 0
|
|
6556
6557
|
};
|
|
6557
6558
|
entries.set(provider, entry);
|
|
6558
6559
|
return entry;
|
|
@@ -6629,6 +6630,9 @@ var summarizeVoiceProviderHealth = async (input) => {
|
|
|
6629
6630
|
}
|
|
6630
6631
|
const entry = applyProviderHealth();
|
|
6631
6632
|
entry.errorCount += 1;
|
|
6633
|
+
if (event.payload.timedOut === true) {
|
|
6634
|
+
entry.timeoutCount += 1;
|
|
6635
|
+
}
|
|
6632
6636
|
entry.lastError = getString(event.payload.error);
|
|
6633
6637
|
entry.lastErrorAt = event.at;
|
|
6634
6638
|
entry.rateLimited ||= event.payload.rateLimited === true;
|
|
@@ -6653,7 +6657,8 @@ var summarizeVoiceProviderHealth = async (input) => {
|
|
|
6653
6657
|
runCount: entry.runCount,
|
|
6654
6658
|
status,
|
|
6655
6659
|
suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
|
|
6656
|
-
suppressedUntil: entry.suppressedUntil
|
|
6660
|
+
suppressedUntil: entry.suppressedUntil,
|
|
6661
|
+
timeoutCount: entry.timeoutCount
|
|
6657
6662
|
};
|
|
6658
6663
|
});
|
|
6659
6664
|
const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
|
|
@@ -6677,6 +6682,7 @@ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p
|
|
|
6677
6682
|
`<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
|
|
6678
6683
|
`<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
|
|
6679
6684
|
`<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
|
|
6685
|
+
`<div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div>`,
|
|
6680
6686
|
`<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
|
|
6681
6687
|
"</dl>",
|
|
6682
6688
|
suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
|
|
@@ -8132,6 +8138,17 @@ var parseJSONValue = (value) => {
|
|
|
8132
8138
|
return value;
|
|
8133
8139
|
}
|
|
8134
8140
|
};
|
|
8141
|
+
|
|
8142
|
+
class VoiceProviderTimeoutError extends Error {
|
|
8143
|
+
provider;
|
|
8144
|
+
timeoutMs;
|
|
8145
|
+
constructor(provider, timeoutMs) {
|
|
8146
|
+
super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
8147
|
+
this.name = "VoiceProviderTimeoutError";
|
|
8148
|
+
this.provider = provider;
|
|
8149
|
+
this.timeoutMs = timeoutMs;
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
8135
8152
|
var getMessageToolCalls = (message) => {
|
|
8136
8153
|
const toolCalls = message.metadata?.toolCalls;
|
|
8137
8154
|
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
@@ -8209,6 +8226,10 @@ var createVoiceProviderRouter = (options) => {
|
|
|
8209
8226
|
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
8210
8227
|
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
8211
8228
|
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
8229
|
+
const getProviderTimeoutMs = (provider) => {
|
|
8230
|
+
const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
|
|
8231
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
8232
|
+
};
|
|
8212
8233
|
const getHealth = (provider) => {
|
|
8213
8234
|
const existing = healthState.get(provider);
|
|
8214
8235
|
if (existing) {
|
|
@@ -8320,6 +8341,25 @@ var createVoiceProviderRouter = (options) => {
|
|
|
8320
8341
|
const emit = async (event, input) => {
|
|
8321
8342
|
await options.onProviderEvent?.(event, input);
|
|
8322
8343
|
};
|
|
8344
|
+
const runProvider = async (provider, model, input) => {
|
|
8345
|
+
const timeoutMs = getProviderTimeoutMs(provider);
|
|
8346
|
+
if (!timeoutMs) {
|
|
8347
|
+
return model.generate(input);
|
|
8348
|
+
}
|
|
8349
|
+
let timeout;
|
|
8350
|
+
try {
|
|
8351
|
+
return await Promise.race([
|
|
8352
|
+
model.generate(input),
|
|
8353
|
+
new Promise((_, reject) => {
|
|
8354
|
+
timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
|
|
8355
|
+
})
|
|
8356
|
+
]);
|
|
8357
|
+
} finally {
|
|
8358
|
+
if (timeout) {
|
|
8359
|
+
clearTimeout(timeout);
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
};
|
|
8323
8363
|
return {
|
|
8324
8364
|
generate: async (input) => {
|
|
8325
8365
|
const { order, selectedProvider } = await resolveOrder(input);
|
|
@@ -8334,12 +8374,14 @@ var createVoiceProviderRouter = (options) => {
|
|
|
8334
8374
|
}
|
|
8335
8375
|
const startedAt = Date.now();
|
|
8336
8376
|
try {
|
|
8337
|
-
const output = await model
|
|
8377
|
+
const output = await runProvider(provider, model, input);
|
|
8338
8378
|
const providerHealth = recordProviderSuccess(provider);
|
|
8339
8379
|
await emit({
|
|
8340
8380
|
at: Date.now(),
|
|
8381
|
+
attempt: index + 1,
|
|
8341
8382
|
elapsedMs: Date.now() - startedAt,
|
|
8342
8383
|
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
8384
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
8343
8385
|
provider,
|
|
8344
8386
|
providerHealth,
|
|
8345
8387
|
recovered: provider !== selectedProvider,
|
|
@@ -8351,22 +8393,26 @@ var createVoiceProviderRouter = (options) => {
|
|
|
8351
8393
|
lastError = error;
|
|
8352
8394
|
const hasNextProvider = index < order.length - 1;
|
|
8353
8395
|
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
8396
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
8354
8397
|
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
8355
8398
|
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
8356
8399
|
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
8357
8400
|
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
8358
8401
|
await emit({
|
|
8359
8402
|
at: Date.now(),
|
|
8403
|
+
attempt: index + 1,
|
|
8360
8404
|
elapsedMs: Date.now() - startedAt,
|
|
8361
8405
|
error: errorMessage(error),
|
|
8362
8406
|
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
8407
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
8363
8408
|
provider,
|
|
8364
8409
|
providerHealth,
|
|
8365
8410
|
rateLimited,
|
|
8366
8411
|
selectedProvider,
|
|
8367
8412
|
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
8368
8413
|
suppressedUntil: providerHealth?.suppressedUntil,
|
|
8369
|
-
status: "error"
|
|
8414
|
+
status: "error",
|
|
8415
|
+
timedOut
|
|
8370
8416
|
}, input);
|
|
8371
8417
|
if (!hasNextProvider || !shouldFallback) {
|
|
8372
8418
|
throw error;
|
|
@@ -8812,6 +8858,288 @@ var createGeminiVoiceAssistantModel = (options) => {
|
|
|
8812
8858
|
}
|
|
8813
8859
|
};
|
|
8814
8860
|
};
|
|
8861
|
+
// src/providerAdapters.ts
|
|
8862
|
+
class VoiceIOProviderTimeoutError extends Error {
|
|
8863
|
+
provider;
|
|
8864
|
+
timeoutMs;
|
|
8865
|
+
constructor(kind, provider, timeoutMs) {
|
|
8866
|
+
super(`Voice ${kind} provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
8867
|
+
this.name = "VoiceIOProviderTimeoutError";
|
|
8868
|
+
this.provider = provider;
|
|
8869
|
+
this.timeoutMs = timeoutMs;
|
|
8870
|
+
}
|
|
8871
|
+
}
|
|
8872
|
+
var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
8873
|
+
var createEmitter = () => {
|
|
8874
|
+
const listeners = new Map;
|
|
8875
|
+
return {
|
|
8876
|
+
emit: async (event, payload) => {
|
|
8877
|
+
await Promise.all([...listeners.get(event) ?? []].map((handler) => Promise.resolve(handler(payload))));
|
|
8878
|
+
},
|
|
8879
|
+
on: (event, handler) => {
|
|
8880
|
+
const set = listeners.get(event) ?? new Set;
|
|
8881
|
+
set.add(handler);
|
|
8882
|
+
listeners.set(event, set);
|
|
8883
|
+
return () => {
|
|
8884
|
+
set.delete(handler);
|
|
8885
|
+
};
|
|
8886
|
+
}
|
|
8887
|
+
};
|
|
8888
|
+
};
|
|
8889
|
+
var getTimeoutMs = (options, provider) => {
|
|
8890
|
+
const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
|
|
8891
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
8892
|
+
};
|
|
8893
|
+
var withTimeout = async (input) => {
|
|
8894
|
+
if (!input.timeoutMs) {
|
|
8895
|
+
return input.run();
|
|
8896
|
+
}
|
|
8897
|
+
let timeout;
|
|
8898
|
+
try {
|
|
8899
|
+
return await Promise.race([
|
|
8900
|
+
Promise.resolve(input.run()),
|
|
8901
|
+
new Promise((_, reject) => {
|
|
8902
|
+
timeout = setTimeout(() => reject(new VoiceIOProviderTimeoutError(input.kind, input.provider, input.timeoutMs)), input.timeoutMs);
|
|
8903
|
+
})
|
|
8904
|
+
]);
|
|
8905
|
+
} finally {
|
|
8906
|
+
if (timeout) {
|
|
8907
|
+
clearTimeout(timeout);
|
|
8908
|
+
}
|
|
8909
|
+
}
|
|
8910
|
+
};
|
|
8911
|
+
var createResolver = (options) => {
|
|
8912
|
+
const providerIds = Object.keys(options.adapters);
|
|
8913
|
+
const firstProvider = providerIds[0];
|
|
8914
|
+
const resolveOrder = async (input) => {
|
|
8915
|
+
const selectedProvider = await options.selectProvider?.(input) ?? firstProvider;
|
|
8916
|
+
const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
|
|
8917
|
+
const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
|
|
8918
|
+
const seen = new Set;
|
|
8919
|
+
const order = candidates.filter((provider) => {
|
|
8920
|
+
if (!provider || seen.has(provider) || !options.adapters[provider]) {
|
|
8921
|
+
return false;
|
|
8922
|
+
}
|
|
8923
|
+
seen.add(provider);
|
|
8924
|
+
return true;
|
|
8925
|
+
});
|
|
8926
|
+
return {
|
|
8927
|
+
order,
|
|
8928
|
+
selectedProvider
|
|
8929
|
+
};
|
|
8930
|
+
};
|
|
8931
|
+
const emit = async (event, input) => {
|
|
8932
|
+
await options.onProviderEvent?.(event, input);
|
|
8933
|
+
};
|
|
8934
|
+
return {
|
|
8935
|
+
emit,
|
|
8936
|
+
providerIds,
|
|
8937
|
+
resolveOrder
|
|
8938
|
+
};
|
|
8939
|
+
};
|
|
8940
|
+
var createVoiceSTTProviderRouter = (options) => {
|
|
8941
|
+
const resolver = createResolver(options);
|
|
8942
|
+
return {
|
|
8943
|
+
kind: "stt",
|
|
8944
|
+
open: async (input) => {
|
|
8945
|
+
const { order, selectedProvider } = await resolver.resolveOrder(input);
|
|
8946
|
+
if (!selectedProvider || order.length === 0) {
|
|
8947
|
+
throw new Error("Voice STT provider router has no available providers.");
|
|
8948
|
+
}
|
|
8949
|
+
let lastError;
|
|
8950
|
+
for (const [index, provider] of order.entries()) {
|
|
8951
|
+
const adapter = options.adapters[provider];
|
|
8952
|
+
if (!adapter) {
|
|
8953
|
+
continue;
|
|
8954
|
+
}
|
|
8955
|
+
const startedAt = Date.now();
|
|
8956
|
+
try {
|
|
8957
|
+
const session = await withTimeout({
|
|
8958
|
+
kind: "stt",
|
|
8959
|
+
operation: "open",
|
|
8960
|
+
provider,
|
|
8961
|
+
run: () => adapter.open(input),
|
|
8962
|
+
timeoutMs: getTimeoutMs(options, provider)
|
|
8963
|
+
});
|
|
8964
|
+
await resolver.emit({
|
|
8965
|
+
at: Date.now(),
|
|
8966
|
+
attempt: index + 1,
|
|
8967
|
+
elapsedMs: Date.now() - startedAt,
|
|
8968
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
8969
|
+
kind: "stt",
|
|
8970
|
+
latencyBudgetMs: getTimeoutMs(options, provider),
|
|
8971
|
+
operation: "open",
|
|
8972
|
+
provider,
|
|
8973
|
+
selectedProvider,
|
|
8974
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
8975
|
+
}, input);
|
|
8976
|
+
return session;
|
|
8977
|
+
} catch (error) {
|
|
8978
|
+
lastError = error;
|
|
8979
|
+
const hasNextProvider = index < order.length - 1;
|
|
8980
|
+
const shouldFallback = options.isProviderError?.(error, provider) ?? true;
|
|
8981
|
+
await resolver.emit({
|
|
8982
|
+
at: Date.now(),
|
|
8983
|
+
attempt: index + 1,
|
|
8984
|
+
elapsedMs: Date.now() - startedAt,
|
|
8985
|
+
error: errorMessage2(error),
|
|
8986
|
+
fallbackProvider: shouldFallback ? order[index + 1] : undefined,
|
|
8987
|
+
kind: "stt",
|
|
8988
|
+
latencyBudgetMs: getTimeoutMs(options, provider),
|
|
8989
|
+
operation: "open",
|
|
8990
|
+
provider,
|
|
8991
|
+
selectedProvider,
|
|
8992
|
+
status: "error",
|
|
8993
|
+
timedOut: error instanceof VoiceIOProviderTimeoutError
|
|
8994
|
+
}, input);
|
|
8995
|
+
if (!hasNextProvider || !shouldFallback) {
|
|
8996
|
+
throw error;
|
|
8997
|
+
}
|
|
8998
|
+
}
|
|
8999
|
+
}
|
|
9000
|
+
throw lastError ?? new Error("Voice STT provider router did not open a provider.");
|
|
9001
|
+
}
|
|
9002
|
+
};
|
|
9003
|
+
};
|
|
9004
|
+
var createVoiceTTSProviderRouter = (options) => {
|
|
9005
|
+
const resolver = createResolver(options);
|
|
9006
|
+
return {
|
|
9007
|
+
kind: "tts",
|
|
9008
|
+
open: async (input) => {
|
|
9009
|
+
const { order, selectedProvider } = await resolver.resolveOrder(input);
|
|
9010
|
+
if (!selectedProvider || order.length === 0) {
|
|
9011
|
+
throw new Error("Voice TTS provider router has no available providers.");
|
|
9012
|
+
}
|
|
9013
|
+
const emitter = createEmitter();
|
|
9014
|
+
let activeSession;
|
|
9015
|
+
let activeProvider;
|
|
9016
|
+
let nextProviderIndex = 0;
|
|
9017
|
+
const attach = (session) => {
|
|
9018
|
+
session.on("audio", (event) => emitter.emit("audio", event));
|
|
9019
|
+
session.on("error", (event) => emitter.emit("error", event));
|
|
9020
|
+
session.on("close", (event) => emitter.emit("close", event));
|
|
9021
|
+
};
|
|
9022
|
+
const openProvider = async (provider, attempt) => {
|
|
9023
|
+
const adapter = options.adapters[provider];
|
|
9024
|
+
if (!adapter) {
|
|
9025
|
+
throw new Error(`Voice TTS provider ${provider} is not configured.`);
|
|
9026
|
+
}
|
|
9027
|
+
const startedAt = Date.now();
|
|
9028
|
+
const session = await withTimeout({
|
|
9029
|
+
kind: "tts",
|
|
9030
|
+
operation: "open",
|
|
9031
|
+
provider,
|
|
9032
|
+
run: () => adapter.open(input),
|
|
9033
|
+
timeoutMs: getTimeoutMs(options, provider)
|
|
9034
|
+
});
|
|
9035
|
+
attach(session);
|
|
9036
|
+
activeSession = session;
|
|
9037
|
+
activeProvider = provider;
|
|
9038
|
+
await resolver.emit({
|
|
9039
|
+
at: Date.now(),
|
|
9040
|
+
attempt,
|
|
9041
|
+
elapsedMs: Date.now() - startedAt,
|
|
9042
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
9043
|
+
kind: "tts",
|
|
9044
|
+
latencyBudgetMs: getTimeoutMs(options, provider),
|
|
9045
|
+
operation: "open",
|
|
9046
|
+
provider,
|
|
9047
|
+
selectedProvider,
|
|
9048
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
9049
|
+
}, input);
|
|
9050
|
+
return session;
|
|
9051
|
+
};
|
|
9052
|
+
const failProvider = async (inputEvent) => {
|
|
9053
|
+
const shouldFallback = options.isProviderError?.(inputEvent.error, inputEvent.provider) ?? true;
|
|
9054
|
+
await resolver.emit({
|
|
9055
|
+
at: Date.now(),
|
|
9056
|
+
attempt: inputEvent.attempt,
|
|
9057
|
+
elapsedMs: Date.now() - inputEvent.startedAt,
|
|
9058
|
+
error: errorMessage2(inputEvent.error),
|
|
9059
|
+
fallbackProvider: shouldFallback ? order[nextProviderIndex] : undefined,
|
|
9060
|
+
kind: "tts",
|
|
9061
|
+
latencyBudgetMs: getTimeoutMs(options, inputEvent.provider),
|
|
9062
|
+
operation: inputEvent.operation,
|
|
9063
|
+
provider: inputEvent.provider,
|
|
9064
|
+
selectedProvider,
|
|
9065
|
+
status: "error",
|
|
9066
|
+
timedOut: inputEvent.error instanceof VoiceIOProviderTimeoutError
|
|
9067
|
+
}, input);
|
|
9068
|
+
return shouldFallback;
|
|
9069
|
+
};
|
|
9070
|
+
for (const [index, provider] of order.entries()) {
|
|
9071
|
+
nextProviderIndex = index + 1;
|
|
9072
|
+
const startedAt = Date.now();
|
|
9073
|
+
try {
|
|
9074
|
+
await openProvider(provider, index + 1);
|
|
9075
|
+
break;
|
|
9076
|
+
} catch (error) {
|
|
9077
|
+
const shouldFallback = await failProvider({
|
|
9078
|
+
attempt: index + 1,
|
|
9079
|
+
error,
|
|
9080
|
+
operation: "open",
|
|
9081
|
+
provider,
|
|
9082
|
+
startedAt
|
|
9083
|
+
});
|
|
9084
|
+
if (!shouldFallback || index >= order.length - 1) {
|
|
9085
|
+
throw error;
|
|
9086
|
+
}
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
if (!activeSession || !activeProvider) {
|
|
9090
|
+
throw new Error("Voice TTS provider router did not open a provider.");
|
|
9091
|
+
}
|
|
9092
|
+
const sendWithFallback = async (text) => {
|
|
9093
|
+
for (;; ) {
|
|
9094
|
+
const session = activeSession;
|
|
9095
|
+
const provider = activeProvider;
|
|
9096
|
+
if (!session || !provider) {
|
|
9097
|
+
throw new Error("Voice TTS provider router has no active provider.");
|
|
9098
|
+
}
|
|
9099
|
+
const startedAt = Date.now();
|
|
9100
|
+
try {
|
|
9101
|
+
await withTimeout({
|
|
9102
|
+
kind: "tts",
|
|
9103
|
+
operation: "send",
|
|
9104
|
+
provider,
|
|
9105
|
+
run: () => session.send(text),
|
|
9106
|
+
timeoutMs: getTimeoutMs(options, provider)
|
|
9107
|
+
});
|
|
9108
|
+
return;
|
|
9109
|
+
} catch (error) {
|
|
9110
|
+
const shouldFallback = await failProvider({
|
|
9111
|
+
attempt: nextProviderIndex,
|
|
9112
|
+
error,
|
|
9113
|
+
operation: "send",
|
|
9114
|
+
provider,
|
|
9115
|
+
startedAt
|
|
9116
|
+
});
|
|
9117
|
+
const nextProvider = order[nextProviderIndex];
|
|
9118
|
+
if (!shouldFallback || !nextProvider) {
|
|
9119
|
+
throw error;
|
|
9120
|
+
}
|
|
9121
|
+
nextProviderIndex += 1;
|
|
9122
|
+
await session.close("tts-provider-fallback").catch(() => {});
|
|
9123
|
+
await openProvider(nextProvider, nextProviderIndex);
|
|
9124
|
+
}
|
|
9125
|
+
}
|
|
9126
|
+
};
|
|
9127
|
+
return {
|
|
9128
|
+
close: async (reason) => {
|
|
9129
|
+
await activeSession?.close(reason);
|
|
9130
|
+
activeSession = undefined;
|
|
9131
|
+
activeProvider = undefined;
|
|
9132
|
+
await emitter.emit("close", {
|
|
9133
|
+
reason,
|
|
9134
|
+
type: "close"
|
|
9135
|
+
});
|
|
9136
|
+
},
|
|
9137
|
+
on: emitter.on,
|
|
9138
|
+
send: sendWithFallback
|
|
9139
|
+
};
|
|
9140
|
+
}
|
|
9141
|
+
};
|
|
9142
|
+
};
|
|
8815
9143
|
// src/sqliteStore.ts
|
|
8816
9144
|
import { Database } from "bun:sqlite";
|
|
8817
9145
|
var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
|
|
@@ -10510,10 +10838,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
|
|
|
10510
10838
|
result.completed += 1;
|
|
10511
10839
|
} catch (error) {
|
|
10512
10840
|
await options.onError?.(error, task);
|
|
10513
|
-
const
|
|
10841
|
+
const errorMessage3 = error instanceof Error ? error.message : String(error);
|
|
10514
10842
|
const failedTask = failVoiceOpsTask(task, {
|
|
10515
10843
|
actor: task.claimedBy ?? "ops-worker",
|
|
10516
|
-
error:
|
|
10844
|
+
error: errorMessage3
|
|
10517
10845
|
});
|
|
10518
10846
|
if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
|
|
10519
10847
|
const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
|
|
@@ -11840,6 +12168,7 @@ export {
|
|
|
11840
12168
|
createVoiceTaskUpdatedEvent,
|
|
11841
12169
|
createVoiceTaskSLABreachedEvent,
|
|
11842
12170
|
createVoiceTaskCreatedEvent,
|
|
12171
|
+
createVoiceTTSProviderRouter,
|
|
11843
12172
|
createVoiceSessionsJSONHandler,
|
|
11844
12173
|
createVoiceSessionsHTMLHandler,
|
|
11845
12174
|
createVoiceSessionReplayRoutes,
|
|
@@ -11849,6 +12178,7 @@ export {
|
|
|
11849
12178
|
createVoiceSessionListRoutes,
|
|
11850
12179
|
createVoiceSession,
|
|
11851
12180
|
createVoiceSTTRoutingCorrectionHandler,
|
|
12181
|
+
createVoiceSTTProviderRouter,
|
|
11852
12182
|
createVoiceSQLiteTraceSinkDeliveryStore,
|
|
11853
12183
|
createVoiceSQLiteTraceEventStore,
|
|
11854
12184
|
createVoiceSQLiteTaskStore,
|
package/dist/modelAdapters.d.ts
CHANGED
|
@@ -36,9 +36,11 @@ export type GeminiVoiceAssistantModelOptions = {
|
|
|
36
36
|
};
|
|
37
37
|
export type VoiceProviderRouterEvent<TProvider extends string = string> = {
|
|
38
38
|
at: number;
|
|
39
|
+
attempt: number;
|
|
39
40
|
elapsedMs: number;
|
|
40
41
|
error?: string;
|
|
41
42
|
fallbackProvider?: TProvider;
|
|
43
|
+
latencyBudgetMs?: number;
|
|
42
44
|
provider: TProvider;
|
|
43
45
|
providerHealth?: VoiceProviderRouterProviderHealth<TProvider>;
|
|
44
46
|
rateLimited?: boolean;
|
|
@@ -47,6 +49,7 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
|
|
|
47
49
|
suppressionRemainingMs?: number;
|
|
48
50
|
suppressedUntil?: number;
|
|
49
51
|
status: 'error' | 'fallback' | 'success';
|
|
52
|
+
timedOut?: boolean;
|
|
50
53
|
};
|
|
51
54
|
export type VoiceProviderRouterFallbackMode = 'never' | 'provider-error' | 'rate-limit';
|
|
52
55
|
export type VoiceProviderRouterPolicy<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TProvider extends string = string> = 'ordered' | 'prefer-cheapest' | 'prefer-fastest' | 'prefer-selected' | {
|
|
@@ -58,6 +61,7 @@ export type VoiceProviderRouterProviderProfile = {
|
|
|
58
61
|
cost?: number;
|
|
59
62
|
latencyMs?: number;
|
|
60
63
|
priority?: number;
|
|
64
|
+
timeoutMs?: number;
|
|
61
65
|
};
|
|
62
66
|
export type VoiceProviderRouterHealthOptions = {
|
|
63
67
|
cooldownMs?: number;
|
|
@@ -79,10 +83,12 @@ export type VoiceProviderRouterOptions<TContext = unknown, TSession extends Voic
|
|
|
79
83
|
fallbackMode?: VoiceProviderRouterFallbackMode;
|
|
80
84
|
isProviderError?: (error: unknown, provider: TProvider) => boolean;
|
|
81
85
|
isRateLimitError?: (error: unknown, provider: TProvider) => boolean;
|
|
86
|
+
isTimeoutError?: (error: unknown, provider: TProvider) => boolean;
|
|
82
87
|
onProviderEvent?: (event: VoiceProviderRouterEvent<TProvider>, input: VoiceAgentModelInput<TContext, TSession>) => Promise<void> | void;
|
|
83
88
|
policy?: VoiceProviderRouterPolicy<TContext, TSession, TProvider>;
|
|
84
89
|
providerHealth?: boolean | VoiceProviderRouterHealthOptions;
|
|
85
90
|
providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
|
|
91
|
+
timeoutMs?: number;
|
|
86
92
|
providers: Partial<Record<TProvider, VoiceAgentModel<TContext, TSession, TResult>>>;
|
|
87
93
|
selectProvider?: (input: VoiceAgentModelInput<TContext, TSession>) => TProvider | undefined | Promise<TProvider | undefined>;
|
|
88
94
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { STTAdapter, STTAdapterOpenOptions, TTSAdapter, TTSAdapterOpenOptions } from './types';
|
|
2
|
+
import type { VoiceProviderRouterProviderProfile } from './modelAdapters';
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
type VoiceIOProviderKind = 'stt' | 'tts';
|
|
5
|
+
type VoiceIOProviderStatus = 'error' | 'fallback' | 'success';
|
|
6
|
+
export type VoiceIOProviderRouterEvent<TProvider extends string = string> = {
|
|
7
|
+
at: number;
|
|
8
|
+
attempt: number;
|
|
9
|
+
elapsedMs: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
fallbackProvider?: TProvider;
|
|
12
|
+
kind: VoiceIOProviderKind;
|
|
13
|
+
latencyBudgetMs?: number;
|
|
14
|
+
operation: 'open' | 'send';
|
|
15
|
+
provider: TProvider;
|
|
16
|
+
selectedProvider: TProvider;
|
|
17
|
+
status: VoiceIOProviderStatus;
|
|
18
|
+
timedOut?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type VoiceIOProviderRouterOptions<TProvider extends string, TAdapter, TOpenOptions> = {
|
|
21
|
+
adapters: Partial<Record<TProvider, TAdapter>>;
|
|
22
|
+
fallback?: readonly TProvider[] | ((input: TOpenOptions) => MaybePromise<readonly TProvider[]>);
|
|
23
|
+
isProviderError?: (error: unknown, provider: TProvider) => boolean;
|
|
24
|
+
onProviderEvent?: (event: VoiceIOProviderRouterEvent<TProvider>, input: TOpenOptions) => Promise<void> | void;
|
|
25
|
+
providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
|
|
26
|
+
selectProvider?: (input: TOpenOptions) => MaybePromise<TProvider | undefined>;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
};
|
|
29
|
+
export type VoiceSTTProviderRouterOptions<TProvider extends string = string, TOptions extends STTAdapterOpenOptions = STTAdapterOpenOptions> = VoiceIOProviderRouterOptions<TProvider, STTAdapter<TOptions>, TOptions>;
|
|
30
|
+
export type VoiceTTSProviderRouterOptions<TProvider extends string = string, TOptions extends TTSAdapterOpenOptions = TTSAdapterOpenOptions> = VoiceIOProviderRouterOptions<TProvider, TTSAdapter<TOptions>, TOptions>;
|
|
31
|
+
export declare const createVoiceSTTProviderRouter: <TProvider extends string = string, TOptions extends STTAdapterOpenOptions = STTAdapterOpenOptions>(options: VoiceSTTProviderRouterOptions<TProvider, TOptions>) => STTAdapter<TOptions>;
|
|
32
|
+
export declare const createVoiceTTSProviderRouter: <TProvider extends string = string, TOptions extends TTSAdapterOpenOptions = TTSAdapterOpenOptions>(options: VoiceTTSProviderRouterOptions<TProvider, TOptions>) => TTSAdapter<TOptions>;
|
|
33
|
+
export {};
|
package/dist/providerHealth.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type VoiceProviderHealthSummary<TProvider extends string = string> = {
|
|
|
15
15
|
status: VoiceProviderHealthStatus;
|
|
16
16
|
suppressionRemainingMs?: number;
|
|
17
17
|
suppressedUntil?: number;
|
|
18
|
+
timeoutCount: number;
|
|
18
19
|
};
|
|
19
20
|
export type VoiceProviderHealthSummaryOptions<TProvider extends string = string> = {
|
|
20
21
|
events?: StoredVoiceTraceEvent[];
|
package/dist/testing/index.js
CHANGED
|
@@ -3601,6 +3601,17 @@ var parseJSONValue = (value) => {
|
|
|
3601
3601
|
return value;
|
|
3602
3602
|
}
|
|
3603
3603
|
};
|
|
3604
|
+
|
|
3605
|
+
class VoiceProviderTimeoutError extends Error {
|
|
3606
|
+
provider;
|
|
3607
|
+
timeoutMs;
|
|
3608
|
+
constructor(provider, timeoutMs) {
|
|
3609
|
+
super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
3610
|
+
this.name = "VoiceProviderTimeoutError";
|
|
3611
|
+
this.provider = provider;
|
|
3612
|
+
this.timeoutMs = timeoutMs;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3604
3615
|
var getMessageToolCalls = (message) => {
|
|
3605
3616
|
const toolCalls = message.metadata?.toolCalls;
|
|
3606
3617
|
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
@@ -3678,6 +3689,10 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3678
3689
|
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
3679
3690
|
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
3680
3691
|
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
3692
|
+
const getProviderTimeoutMs = (provider) => {
|
|
3693
|
+
const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
|
|
3694
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
3695
|
+
};
|
|
3681
3696
|
const getHealth = (provider) => {
|
|
3682
3697
|
const existing = healthState.get(provider);
|
|
3683
3698
|
if (existing) {
|
|
@@ -3789,6 +3804,25 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3789
3804
|
const emit = async (event, input) => {
|
|
3790
3805
|
await options.onProviderEvent?.(event, input);
|
|
3791
3806
|
};
|
|
3807
|
+
const runProvider = async (provider, model, input) => {
|
|
3808
|
+
const timeoutMs = getProviderTimeoutMs(provider);
|
|
3809
|
+
if (!timeoutMs) {
|
|
3810
|
+
return model.generate(input);
|
|
3811
|
+
}
|
|
3812
|
+
let timeout;
|
|
3813
|
+
try {
|
|
3814
|
+
return await Promise.race([
|
|
3815
|
+
model.generate(input),
|
|
3816
|
+
new Promise((_, reject) => {
|
|
3817
|
+
timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
|
|
3818
|
+
})
|
|
3819
|
+
]);
|
|
3820
|
+
} finally {
|
|
3821
|
+
if (timeout) {
|
|
3822
|
+
clearTimeout(timeout);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
};
|
|
3792
3826
|
return {
|
|
3793
3827
|
generate: async (input) => {
|
|
3794
3828
|
const { order, selectedProvider } = await resolveOrder(input);
|
|
@@ -3803,12 +3837,14 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3803
3837
|
}
|
|
3804
3838
|
const startedAt = Date.now();
|
|
3805
3839
|
try {
|
|
3806
|
-
const output = await model
|
|
3840
|
+
const output = await runProvider(provider, model, input);
|
|
3807
3841
|
const providerHealth = recordProviderSuccess(provider);
|
|
3808
3842
|
await emit({
|
|
3809
3843
|
at: Date.now(),
|
|
3844
|
+
attempt: index + 1,
|
|
3810
3845
|
elapsedMs: Date.now() - startedAt,
|
|
3811
3846
|
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
3847
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
3812
3848
|
provider,
|
|
3813
3849
|
providerHealth,
|
|
3814
3850
|
recovered: provider !== selectedProvider,
|
|
@@ -3820,22 +3856,26 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3820
3856
|
lastError = error;
|
|
3821
3857
|
const hasNextProvider = index < order.length - 1;
|
|
3822
3858
|
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
3859
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
3823
3860
|
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
3824
3861
|
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
3825
3862
|
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
3826
3863
|
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
3827
3864
|
await emit({
|
|
3828
3865
|
at: Date.now(),
|
|
3866
|
+
attempt: index + 1,
|
|
3829
3867
|
elapsedMs: Date.now() - startedAt,
|
|
3830
3868
|
error: errorMessage(error),
|
|
3831
3869
|
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
3870
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
3832
3871
|
provider,
|
|
3833
3872
|
providerHealth,
|
|
3834
3873
|
rateLimited,
|
|
3835
3874
|
selectedProvider,
|
|
3836
3875
|
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
3837
3876
|
suppressedUntil: providerHealth?.suppressedUntil,
|
|
3838
|
-
status: "error"
|
|
3877
|
+
status: "error",
|
|
3878
|
+
timedOut
|
|
3839
3879
|
}, input);
|
|
3840
3880
|
if (!hasNextProvider || !shouldFallback) {
|
|
3841
3881
|
throw error;
|