@absolutejs/voice 0.0.22-beta.16 → 0.0.22-beta.17
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/testing/index.d.ts
CHANGED
package/dist/testing/index.js
CHANGED
|
@@ -3468,6 +3468,778 @@ var loadVoiceTestFixtures = async (fixtureDirectory) => {
|
|
|
3468
3468
|
}
|
|
3469
3469
|
return fixtures;
|
|
3470
3470
|
};
|
|
3471
|
+
// src/modelAdapters.ts
|
|
3472
|
+
var OUTPUT_SCHEMA = {
|
|
3473
|
+
additionalProperties: false,
|
|
3474
|
+
properties: {
|
|
3475
|
+
assistantText: {
|
|
3476
|
+
type: "string"
|
|
3477
|
+
},
|
|
3478
|
+
complete: {
|
|
3479
|
+
type: "boolean"
|
|
3480
|
+
},
|
|
3481
|
+
escalate: {
|
|
3482
|
+
additionalProperties: false,
|
|
3483
|
+
properties: {
|
|
3484
|
+
metadata: {
|
|
3485
|
+
additionalProperties: true,
|
|
3486
|
+
type: "object"
|
|
3487
|
+
},
|
|
3488
|
+
reason: {
|
|
3489
|
+
type: "string"
|
|
3490
|
+
}
|
|
3491
|
+
},
|
|
3492
|
+
required: ["reason"],
|
|
3493
|
+
type: "object"
|
|
3494
|
+
},
|
|
3495
|
+
noAnswer: {
|
|
3496
|
+
additionalProperties: false,
|
|
3497
|
+
properties: {
|
|
3498
|
+
metadata: {
|
|
3499
|
+
additionalProperties: true,
|
|
3500
|
+
type: "object"
|
|
3501
|
+
}
|
|
3502
|
+
},
|
|
3503
|
+
type: "object"
|
|
3504
|
+
},
|
|
3505
|
+
result: {
|
|
3506
|
+
additionalProperties: true,
|
|
3507
|
+
type: "object"
|
|
3508
|
+
},
|
|
3509
|
+
transfer: {
|
|
3510
|
+
additionalProperties: false,
|
|
3511
|
+
properties: {
|
|
3512
|
+
metadata: {
|
|
3513
|
+
additionalProperties: true,
|
|
3514
|
+
type: "object"
|
|
3515
|
+
},
|
|
3516
|
+
reason: {
|
|
3517
|
+
type: "string"
|
|
3518
|
+
},
|
|
3519
|
+
target: {
|
|
3520
|
+
type: "string"
|
|
3521
|
+
}
|
|
3522
|
+
},
|
|
3523
|
+
required: ["target"],
|
|
3524
|
+
type: "object"
|
|
3525
|
+
},
|
|
3526
|
+
voicemail: {
|
|
3527
|
+
additionalProperties: false,
|
|
3528
|
+
properties: {
|
|
3529
|
+
metadata: {
|
|
3530
|
+
additionalProperties: true,
|
|
3531
|
+
type: "object"
|
|
3532
|
+
}
|
|
3533
|
+
},
|
|
3534
|
+
type: "object"
|
|
3535
|
+
}
|
|
3536
|
+
},
|
|
3537
|
+
type: "object"
|
|
3538
|
+
};
|
|
3539
|
+
var ROUTE_RESULT_INSTRUCTION = "Return only a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools. Only set transfer, escalate, voicemail, or noAnswer when the user explicitly asks for that lifecycle outcome or a tool result says that exact outcome. Do not infer voicemail from generic words like voice, voice app, or voice integration.";
|
|
3540
|
+
var stripJSONCodeFence = (value) => {
|
|
3541
|
+
const trimmed = value.trim();
|
|
3542
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
3543
|
+
return match?.[1]?.trim() ?? value;
|
|
3544
|
+
};
|
|
3545
|
+
var parseJSON = (value) => {
|
|
3546
|
+
try {
|
|
3547
|
+
const parsed = JSON.parse(stripJSONCodeFence(value));
|
|
3548
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3549
|
+
} catch {
|
|
3550
|
+
return {
|
|
3551
|
+
assistantText: value
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
};
|
|
3555
|
+
var parseJSONValue = (value) => {
|
|
3556
|
+
try {
|
|
3557
|
+
return JSON.parse(value);
|
|
3558
|
+
} catch {
|
|
3559
|
+
return value;
|
|
3560
|
+
}
|
|
3561
|
+
};
|
|
3562
|
+
var getMessageToolCalls = (message) => {
|
|
3563
|
+
const toolCalls = message.metadata?.toolCalls;
|
|
3564
|
+
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
3565
|
+
};
|
|
3566
|
+
var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
|
|
3567
|
+
var sleep = (ms) => new Promise((resolve2) => {
|
|
3568
|
+
setTimeout(resolve2, ms);
|
|
3569
|
+
});
|
|
3570
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
3571
|
+
var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
|
|
3572
|
+
var normalizeRouteOutput = (output) => {
|
|
3573
|
+
const result = {};
|
|
3574
|
+
if (typeof output.assistantText === "string") {
|
|
3575
|
+
result.assistantText = output.assistantText;
|
|
3576
|
+
}
|
|
3577
|
+
if (typeof output.complete === "boolean") {
|
|
3578
|
+
result.complete = output.complete;
|
|
3579
|
+
}
|
|
3580
|
+
if (output.result !== undefined) {
|
|
3581
|
+
result.result = output.result;
|
|
3582
|
+
}
|
|
3583
|
+
if (output.transfer && typeof output.transfer === "object") {
|
|
3584
|
+
const transfer = output.transfer;
|
|
3585
|
+
if (typeof transfer.target === "string") {
|
|
3586
|
+
result.transfer = {
|
|
3587
|
+
metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
|
|
3588
|
+
reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
|
|
3589
|
+
target: transfer.target
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (output.escalate && typeof output.escalate === "object") {
|
|
3594
|
+
const escalate = output.escalate;
|
|
3595
|
+
if (typeof escalate.reason === "string") {
|
|
3596
|
+
result.escalate = {
|
|
3597
|
+
metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
|
|
3598
|
+
reason: escalate.reason
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
if (output.voicemail && typeof output.voicemail === "object") {
|
|
3603
|
+
const voicemail = output.voicemail;
|
|
3604
|
+
result.voicemail = {
|
|
3605
|
+
metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
if (output.noAnswer && typeof output.noAnswer === "object") {
|
|
3609
|
+
const noAnswer = output.noAnswer;
|
|
3610
|
+
result.noAnswer = {
|
|
3611
|
+
metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
return result;
|
|
3615
|
+
};
|
|
3616
|
+
var createJSONVoiceAssistantModel = (options) => ({
|
|
3617
|
+
generate: async (input) => {
|
|
3618
|
+
const output = await options.generate(input);
|
|
3619
|
+
if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
|
|
3620
|
+
return output;
|
|
3621
|
+
}
|
|
3622
|
+
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
3623
|
+
}
|
|
3624
|
+
});
|
|
3625
|
+
var createVoiceProviderRouter = (options) => {
|
|
3626
|
+
const providerIds = Object.keys(options.providers);
|
|
3627
|
+
const firstProvider = providerIds[0];
|
|
3628
|
+
const policy = typeof options.policy === "string" ? {
|
|
3629
|
+
strategy: options.policy
|
|
3630
|
+
} : options.policy;
|
|
3631
|
+
const strategy = policy?.strategy ?? "prefer-selected";
|
|
3632
|
+
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
|
|
3633
|
+
const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
|
|
3634
|
+
const healthState = new Map;
|
|
3635
|
+
const now = () => healthOptions?.now?.() ?? Date.now();
|
|
3636
|
+
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
3637
|
+
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
3638
|
+
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
3639
|
+
const getHealth = (provider) => {
|
|
3640
|
+
const existing = healthState.get(provider);
|
|
3641
|
+
if (existing) {
|
|
3642
|
+
return existing;
|
|
3643
|
+
}
|
|
3644
|
+
const next = {
|
|
3645
|
+
consecutiveFailures: 0,
|
|
3646
|
+
provider,
|
|
3647
|
+
status: "healthy"
|
|
3648
|
+
};
|
|
3649
|
+
healthState.set(provider, next);
|
|
3650
|
+
return next;
|
|
3651
|
+
};
|
|
3652
|
+
const cloneHealth = (provider) => {
|
|
3653
|
+
if (!healthOptions) {
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
return {
|
|
3657
|
+
...getHealth(provider)
|
|
3658
|
+
};
|
|
3659
|
+
};
|
|
3660
|
+
const getSuppressionRemainingMs = (provider) => {
|
|
3661
|
+
if (!healthOptions) {
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
const suppressedUntil = getHealth(provider).suppressedUntil;
|
|
3665
|
+
return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
|
|
3666
|
+
};
|
|
3667
|
+
const isSuppressed = (provider) => {
|
|
3668
|
+
if (!healthOptions) {
|
|
3669
|
+
return false;
|
|
3670
|
+
}
|
|
3671
|
+
const health = getHealth(provider);
|
|
3672
|
+
return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
|
|
3673
|
+
};
|
|
3674
|
+
const recordProviderSuccess = (provider) => {
|
|
3675
|
+
if (!healthOptions) {
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
const health = getHealth(provider);
|
|
3679
|
+
health.consecutiveFailures = 0;
|
|
3680
|
+
health.status = "healthy";
|
|
3681
|
+
health.suppressedUntil = undefined;
|
|
3682
|
+
return cloneHealth(provider);
|
|
3683
|
+
};
|
|
3684
|
+
const recordProviderError = (provider, isProviderError, rateLimited) => {
|
|
3685
|
+
if (!healthOptions || !isProviderError) {
|
|
3686
|
+
return cloneHealth(provider);
|
|
3687
|
+
}
|
|
3688
|
+
const currentTime = now();
|
|
3689
|
+
const health = getHealth(provider);
|
|
3690
|
+
health.consecutiveFailures += 1;
|
|
3691
|
+
health.lastFailureAt = currentTime;
|
|
3692
|
+
if (rateLimited) {
|
|
3693
|
+
health.lastRateLimitedAt = currentTime;
|
|
3694
|
+
}
|
|
3695
|
+
if (rateLimited || health.consecutiveFailures >= failureThreshold) {
|
|
3696
|
+
health.status = "suppressed";
|
|
3697
|
+
health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
|
|
3698
|
+
}
|
|
3699
|
+
return cloneHealth(provider);
|
|
3700
|
+
};
|
|
3701
|
+
const resolveAllowedProviders = async (input) => {
|
|
3702
|
+
const allowProviders = policy?.allowProviders ?? options.allowProviders;
|
|
3703
|
+
const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
|
|
3704
|
+
return new Set(allowed ?? providerIds);
|
|
3705
|
+
};
|
|
3706
|
+
const sortProviders = (providers) => {
|
|
3707
|
+
if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
|
|
3708
|
+
return providers;
|
|
3709
|
+
}
|
|
3710
|
+
return [...providers].sort((left, right) => {
|
|
3711
|
+
const leftProfile = options.providerProfiles?.[left];
|
|
3712
|
+
const rightProfile = options.providerProfiles?.[right];
|
|
3713
|
+
const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
3714
|
+
const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
|
|
3715
|
+
return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
|
|
3716
|
+
});
|
|
3717
|
+
};
|
|
3718
|
+
const resolveOrder = async (input) => {
|
|
3719
|
+
const selectedProvider = await options.selectProvider?.(input);
|
|
3720
|
+
const allowedProviders = await resolveAllowedProviders(input);
|
|
3721
|
+
const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
|
|
3722
|
+
const rankedProviders = sortProviders([
|
|
3723
|
+
...fallbackOrder ?? providerIds
|
|
3724
|
+
]).filter((provider) => allowedProviders.has(provider));
|
|
3725
|
+
const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
|
|
3726
|
+
const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
|
|
3727
|
+
const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
|
|
3728
|
+
const seen = new Set;
|
|
3729
|
+
const order = [];
|
|
3730
|
+
const candidates = strategy === "ordered" ? candidateRankedProviders : [
|
|
3731
|
+
preferred,
|
|
3732
|
+
...candidateRankedProviders,
|
|
3733
|
+
...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
|
|
3734
|
+
];
|
|
3735
|
+
for (const provider of candidates) {
|
|
3736
|
+
if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
|
|
3737
|
+
continue;
|
|
3738
|
+
}
|
|
3739
|
+
seen.add(provider);
|
|
3740
|
+
order.push(provider);
|
|
3741
|
+
}
|
|
3742
|
+
return {
|
|
3743
|
+
order,
|
|
3744
|
+
selectedProvider: preferred
|
|
3745
|
+
};
|
|
3746
|
+
};
|
|
3747
|
+
const emit = async (event, input) => {
|
|
3748
|
+
await options.onProviderEvent?.(event, input);
|
|
3749
|
+
};
|
|
3750
|
+
return {
|
|
3751
|
+
generate: async (input) => {
|
|
3752
|
+
const { order, selectedProvider } = await resolveOrder(input);
|
|
3753
|
+
if (!selectedProvider || order.length === 0) {
|
|
3754
|
+
throw new Error("Voice provider router has no available providers.");
|
|
3755
|
+
}
|
|
3756
|
+
let lastError;
|
|
3757
|
+
for (const [index, provider] of order.entries()) {
|
|
3758
|
+
const model = options.providers[provider];
|
|
3759
|
+
if (!model) {
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
const startedAt = Date.now();
|
|
3763
|
+
try {
|
|
3764
|
+
const output = await model.generate(input);
|
|
3765
|
+
const providerHealth = recordProviderSuccess(provider);
|
|
3766
|
+
await emit({
|
|
3767
|
+
at: Date.now(),
|
|
3768
|
+
elapsedMs: Date.now() - startedAt,
|
|
3769
|
+
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
3770
|
+
provider,
|
|
3771
|
+
providerHealth,
|
|
3772
|
+
recovered: provider !== selectedProvider,
|
|
3773
|
+
selectedProvider,
|
|
3774
|
+
status: provider === selectedProvider ? "success" : "fallback"
|
|
3775
|
+
}, input);
|
|
3776
|
+
return output;
|
|
3777
|
+
} catch (error) {
|
|
3778
|
+
lastError = error;
|
|
3779
|
+
const hasNextProvider = index < order.length - 1;
|
|
3780
|
+
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
3781
|
+
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
3782
|
+
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
3783
|
+
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
3784
|
+
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
3785
|
+
await emit({
|
|
3786
|
+
at: Date.now(),
|
|
3787
|
+
elapsedMs: Date.now() - startedAt,
|
|
3788
|
+
error: errorMessage(error),
|
|
3789
|
+
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
3790
|
+
provider,
|
|
3791
|
+
providerHealth,
|
|
3792
|
+
rateLimited,
|
|
3793
|
+
selectedProvider,
|
|
3794
|
+
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
3795
|
+
suppressedUntil: providerHealth?.suppressedUntil,
|
|
3796
|
+
status: "error"
|
|
3797
|
+
}, input);
|
|
3798
|
+
if (!hasNextProvider || !shouldFallback) {
|
|
3799
|
+
throw error;
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
throw lastError ?? new Error("Voice provider router did not run a provider.");
|
|
3804
|
+
}
|
|
3805
|
+
};
|
|
3806
|
+
};
|
|
3807
|
+
var messageToOpenAIInput = (message) => {
|
|
3808
|
+
if (message.role === "tool") {
|
|
3809
|
+
return [
|
|
3810
|
+
{
|
|
3811
|
+
call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
|
|
3812
|
+
output: message.content,
|
|
3813
|
+
type: "function_call_output"
|
|
3814
|
+
}
|
|
3815
|
+
];
|
|
3816
|
+
}
|
|
3817
|
+
const toolCalls = getMessageToolCalls(message);
|
|
3818
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
3819
|
+
return toolCalls.map((toolCall) => ({
|
|
3820
|
+
arguments: JSON.stringify(toolCall.args),
|
|
3821
|
+
call_id: toolCall.id ?? crypto.randomUUID(),
|
|
3822
|
+
name: toolCall.name,
|
|
3823
|
+
type: "function_call"
|
|
3824
|
+
}));
|
|
3825
|
+
}
|
|
3826
|
+
return [
|
|
3827
|
+
{
|
|
3828
|
+
content: message.content,
|
|
3829
|
+
role: message.role === "system" ? "developer" : message.role
|
|
3830
|
+
}
|
|
3831
|
+
];
|
|
3832
|
+
};
|
|
3833
|
+
var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
|
|
3834
|
+
var messageToAnthropicMessage = (message) => {
|
|
3835
|
+
if (message.role === "system") {
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
if (message.role === "tool") {
|
|
3839
|
+
if (!message.toolCallId) {
|
|
3840
|
+
return {
|
|
3841
|
+
content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
|
|
3842
|
+
role: "user"
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
return {
|
|
3846
|
+
content: [
|
|
3847
|
+
{
|
|
3848
|
+
content: message.content,
|
|
3849
|
+
tool_use_id: message.toolCallId,
|
|
3850
|
+
type: "tool_result"
|
|
3851
|
+
}
|
|
3852
|
+
],
|
|
3853
|
+
role: "user"
|
|
3854
|
+
};
|
|
3855
|
+
}
|
|
3856
|
+
const toolCalls = getMessageToolCalls(message);
|
|
3857
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
3858
|
+
return {
|
|
3859
|
+
content: [
|
|
3860
|
+
...message.content ? [
|
|
3861
|
+
{
|
|
3862
|
+
text: message.content,
|
|
3863
|
+
type: "text"
|
|
3864
|
+
}
|
|
3865
|
+
] : [],
|
|
3866
|
+
...toolCalls.map((toolCall) => ({
|
|
3867
|
+
id: toolCall.id ?? crypto.randomUUID(),
|
|
3868
|
+
input: toolCall.args,
|
|
3869
|
+
name: toolCall.name,
|
|
3870
|
+
type: "tool_use"
|
|
3871
|
+
}))
|
|
3872
|
+
],
|
|
3873
|
+
role: "assistant"
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
return {
|
|
3877
|
+
content: message.content,
|
|
3878
|
+
role: message.role
|
|
3879
|
+
};
|
|
3880
|
+
};
|
|
3881
|
+
var toGeminiSchema = (schema) => {
|
|
3882
|
+
const next = {};
|
|
3883
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
3884
|
+
if (key === "additionalProperties") {
|
|
3885
|
+
continue;
|
|
3886
|
+
}
|
|
3887
|
+
if (key === "type" && typeof value === "string") {
|
|
3888
|
+
next[key] = value.toUpperCase();
|
|
3889
|
+
continue;
|
|
3890
|
+
}
|
|
3891
|
+
if (Array.isArray(value)) {
|
|
3892
|
+
next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
|
|
3893
|
+
continue;
|
|
3894
|
+
}
|
|
3895
|
+
if (value && typeof value === "object") {
|
|
3896
|
+
next[key] = toGeminiSchema(value);
|
|
3897
|
+
continue;
|
|
3898
|
+
}
|
|
3899
|
+
next[key] = value;
|
|
3900
|
+
}
|
|
3901
|
+
return next;
|
|
3902
|
+
};
|
|
3903
|
+
var messageToGeminiContent = (message) => {
|
|
3904
|
+
if (message.role === "system") {
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
if (message.role === "tool") {
|
|
3908
|
+
return {
|
|
3909
|
+
parts: [
|
|
3910
|
+
{
|
|
3911
|
+
functionResponse: {
|
|
3912
|
+
id: message.toolCallId,
|
|
3913
|
+
name: message.name ?? "tool",
|
|
3914
|
+
response: {
|
|
3915
|
+
result: parseJSONValue(message.content)
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
],
|
|
3920
|
+
role: "user"
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
const toolCalls = getMessageToolCalls(message);
|
|
3924
|
+
if (message.role === "assistant" && toolCalls.length) {
|
|
3925
|
+
return {
|
|
3926
|
+
parts: [
|
|
3927
|
+
...message.content ? [
|
|
3928
|
+
{
|
|
3929
|
+
text: message.content
|
|
3930
|
+
}
|
|
3931
|
+
] : [],
|
|
3932
|
+
...toolCalls.map((toolCall) => ({
|
|
3933
|
+
functionCall: {
|
|
3934
|
+
args: toolCall.args,
|
|
3935
|
+
id: toolCall.id,
|
|
3936
|
+
name: toolCall.name
|
|
3937
|
+
}
|
|
3938
|
+
}))
|
|
3939
|
+
],
|
|
3940
|
+
role: "model"
|
|
3941
|
+
};
|
|
3942
|
+
}
|
|
3943
|
+
return {
|
|
3944
|
+
parts: [
|
|
3945
|
+
{
|
|
3946
|
+
text: message.content
|
|
3947
|
+
}
|
|
3948
|
+
],
|
|
3949
|
+
role: message.role === "assistant" ? "model" : "user"
|
|
3950
|
+
};
|
|
3951
|
+
};
|
|
3952
|
+
var extractText = (response) => {
|
|
3953
|
+
if (typeof response.output_text === "string") {
|
|
3954
|
+
return response.output_text;
|
|
3955
|
+
}
|
|
3956
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
3957
|
+
for (const item of output) {
|
|
3958
|
+
if (!item || typeof item !== "object") {
|
|
3959
|
+
continue;
|
|
3960
|
+
}
|
|
3961
|
+
const record = item;
|
|
3962
|
+
const content = Array.isArray(record.content) ? record.content : [];
|
|
3963
|
+
for (const contentItem of content) {
|
|
3964
|
+
if (!contentItem || typeof contentItem !== "object") {
|
|
3965
|
+
continue;
|
|
3966
|
+
}
|
|
3967
|
+
const contentRecord = contentItem;
|
|
3968
|
+
if (typeof contentRecord.text === "string") {
|
|
3969
|
+
return contentRecord.text;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
return "";
|
|
3974
|
+
};
|
|
3975
|
+
var extractToolCalls = (response) => {
|
|
3976
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
3977
|
+
const toolCalls = [];
|
|
3978
|
+
for (const item of output) {
|
|
3979
|
+
if (!item || typeof item !== "object") {
|
|
3980
|
+
continue;
|
|
3981
|
+
}
|
|
3982
|
+
const record = item;
|
|
3983
|
+
if (record.type !== "function_call" || typeof record.name !== "string") {
|
|
3984
|
+
continue;
|
|
3985
|
+
}
|
|
3986
|
+
const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
|
|
3987
|
+
toolCalls.push({
|
|
3988
|
+
args,
|
|
3989
|
+
id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
|
|
3990
|
+
name: record.name
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
return toolCalls;
|
|
3994
|
+
};
|
|
3995
|
+
var createOpenAIVoiceAssistantModel = (options) => {
|
|
3996
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3997
|
+
const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
3998
|
+
const model = options.model ?? "gpt-4.1-mini";
|
|
3999
|
+
return {
|
|
4000
|
+
generate: async (input) => {
|
|
4001
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
|
|
4002
|
+
body: JSON.stringify({
|
|
4003
|
+
input: messagesToOpenAIInput(input.messages),
|
|
4004
|
+
instructions: [
|
|
4005
|
+
input.system,
|
|
4006
|
+
"Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
|
|
4007
|
+
].filter(Boolean).join(`
|
|
4008
|
+
|
|
4009
|
+
`),
|
|
4010
|
+
max_output_tokens: options.maxOutputTokens,
|
|
4011
|
+
model,
|
|
4012
|
+
temperature: options.temperature,
|
|
4013
|
+
text: {
|
|
4014
|
+
format: {
|
|
4015
|
+
name: "voice_route_result",
|
|
4016
|
+
schema: OUTPUT_SCHEMA,
|
|
4017
|
+
strict: false,
|
|
4018
|
+
type: "json_schema"
|
|
4019
|
+
}
|
|
4020
|
+
},
|
|
4021
|
+
tool_choice: input.tools.length ? "auto" : "none",
|
|
4022
|
+
tools: input.tools.map((tool) => ({
|
|
4023
|
+
description: tool.description,
|
|
4024
|
+
name: tool.name,
|
|
4025
|
+
parameters: tool.parameters ?? {
|
|
4026
|
+
additionalProperties: true,
|
|
4027
|
+
type: "object"
|
|
4028
|
+
},
|
|
4029
|
+
strict: false,
|
|
4030
|
+
type: "function"
|
|
4031
|
+
}))
|
|
4032
|
+
}),
|
|
4033
|
+
headers: {
|
|
4034
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
4035
|
+
"content-type": "application/json"
|
|
4036
|
+
},
|
|
4037
|
+
method: "POST"
|
|
4038
|
+
});
|
|
4039
|
+
if (!response.ok) {
|
|
4040
|
+
throw createHTTPError("OpenAI", response);
|
|
4041
|
+
}
|
|
4042
|
+
const body = await response.json();
|
|
4043
|
+
if (body.usage && typeof body.usage === "object") {
|
|
4044
|
+
await options.onUsage?.(body.usage);
|
|
4045
|
+
}
|
|
4046
|
+
const toolCalls = extractToolCalls(body);
|
|
4047
|
+
if (toolCalls.length) {
|
|
4048
|
+
return {
|
|
4049
|
+
toolCalls
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
return normalizeRouteOutput(parseJSON(extractText(body)));
|
|
4053
|
+
}
|
|
4054
|
+
};
|
|
4055
|
+
};
|
|
4056
|
+
var extractAnthropicText = (response) => {
|
|
4057
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
4058
|
+
return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
|
|
4059
|
+
`);
|
|
4060
|
+
};
|
|
4061
|
+
var extractAnthropicToolCalls = (response) => {
|
|
4062
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
4063
|
+
const toolCalls = [];
|
|
4064
|
+
for (const item of content) {
|
|
4065
|
+
if (!item || typeof item !== "object") {
|
|
4066
|
+
continue;
|
|
4067
|
+
}
|
|
4068
|
+
const record = item;
|
|
4069
|
+
if (record.type !== "tool_use" || typeof record.name !== "string") {
|
|
4070
|
+
continue;
|
|
4071
|
+
}
|
|
4072
|
+
toolCalls.push({
|
|
4073
|
+
args: record.input && typeof record.input === "object" ? record.input : {},
|
|
4074
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
4075
|
+
name: record.name
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
return toolCalls;
|
|
4079
|
+
};
|
|
4080
|
+
var createAnthropicVoiceAssistantModel = (options) => {
|
|
4081
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4082
|
+
const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
4083
|
+
const model = options.model ?? "claude-sonnet-4-5";
|
|
4084
|
+
return {
|
|
4085
|
+
generate: async (input) => {
|
|
4086
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
|
|
4087
|
+
body: JSON.stringify({
|
|
4088
|
+
max_tokens: options.maxOutputTokens ?? 1024,
|
|
4089
|
+
messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
|
|
4090
|
+
model,
|
|
4091
|
+
system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
4092
|
+
|
|
4093
|
+
`),
|
|
4094
|
+
temperature: options.temperature,
|
|
4095
|
+
tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
|
|
4096
|
+
tools: input.tools.map((tool) => ({
|
|
4097
|
+
description: tool.description,
|
|
4098
|
+
input_schema: tool.parameters ?? {
|
|
4099
|
+
additionalProperties: true,
|
|
4100
|
+
type: "object"
|
|
4101
|
+
},
|
|
4102
|
+
name: tool.name
|
|
4103
|
+
}))
|
|
4104
|
+
}),
|
|
4105
|
+
headers: {
|
|
4106
|
+
"anthropic-version": options.version ?? "2023-06-01",
|
|
4107
|
+
"content-type": "application/json",
|
|
4108
|
+
"x-api-key": options.apiKey
|
|
4109
|
+
},
|
|
4110
|
+
method: "POST"
|
|
4111
|
+
});
|
|
4112
|
+
if (!response.ok) {
|
|
4113
|
+
throw createHTTPError("Anthropic", response);
|
|
4114
|
+
}
|
|
4115
|
+
const body = await response.json();
|
|
4116
|
+
if (body.usage && typeof body.usage === "object") {
|
|
4117
|
+
await options.onUsage?.(body.usage);
|
|
4118
|
+
}
|
|
4119
|
+
const toolCalls = extractAnthropicToolCalls(body);
|
|
4120
|
+
if (toolCalls.length) {
|
|
4121
|
+
return {
|
|
4122
|
+
assistantText: extractAnthropicText(body) || undefined,
|
|
4123
|
+
toolCalls
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
4129
|
+
};
|
|
4130
|
+
var extractGeminiCandidateParts = (response) => {
|
|
4131
|
+
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
|
|
4132
|
+
const first = candidates[0];
|
|
4133
|
+
if (!first || typeof first !== "object") {
|
|
4134
|
+
return [];
|
|
4135
|
+
}
|
|
4136
|
+
const content = first.content;
|
|
4137
|
+
if (!content || typeof content !== "object") {
|
|
4138
|
+
return [];
|
|
4139
|
+
}
|
|
4140
|
+
const parts = content.parts;
|
|
4141
|
+
return Array.isArray(parts) ? parts : [];
|
|
4142
|
+
};
|
|
4143
|
+
var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
4144
|
+
`);
|
|
4145
|
+
var extractGeminiToolCalls = (response) => {
|
|
4146
|
+
const toolCalls = [];
|
|
4147
|
+
for (const part of extractGeminiCandidateParts(response)) {
|
|
4148
|
+
if (!part || typeof part !== "object") {
|
|
4149
|
+
continue;
|
|
4150
|
+
}
|
|
4151
|
+
const functionCall = part.functionCall;
|
|
4152
|
+
if (!functionCall || typeof functionCall !== "object") {
|
|
4153
|
+
continue;
|
|
4154
|
+
}
|
|
4155
|
+
const record = functionCall;
|
|
4156
|
+
if (typeof record.name !== "string") {
|
|
4157
|
+
continue;
|
|
4158
|
+
}
|
|
4159
|
+
toolCalls.push({
|
|
4160
|
+
args: record.args && typeof record.args === "object" ? record.args : {},
|
|
4161
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
4162
|
+
name: record.name
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
return toolCalls;
|
|
4166
|
+
};
|
|
4167
|
+
var createGeminiVoiceAssistantModel = (options) => {
|
|
4168
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4169
|
+
const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
4170
|
+
const model = options.model ?? "gemini-2.5-flash";
|
|
4171
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
4172
|
+
return {
|
|
4173
|
+
generate: async (input) => {
|
|
4174
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
4175
|
+
let response;
|
|
4176
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
4177
|
+
response = await fetchImpl(endpoint, {
|
|
4178
|
+
body: JSON.stringify({
|
|
4179
|
+
contents: input.messages.map(messageToGeminiContent).filter(Boolean),
|
|
4180
|
+
generationConfig: {
|
|
4181
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
4182
|
+
...input.tools.length ? {} : {
|
|
4183
|
+
responseMimeType: "application/json",
|
|
4184
|
+
responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
|
|
4185
|
+
},
|
|
4186
|
+
temperature: options.temperature
|
|
4187
|
+
},
|
|
4188
|
+
systemInstruction: {
|
|
4189
|
+
parts: [
|
|
4190
|
+
{
|
|
4191
|
+
text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
|
|
4192
|
+
|
|
4193
|
+
`)
|
|
4194
|
+
}
|
|
4195
|
+
]
|
|
4196
|
+
},
|
|
4197
|
+
tools: input.tools.length ? [
|
|
4198
|
+
{
|
|
4199
|
+
functionDeclarations: input.tools.map((tool) => ({
|
|
4200
|
+
description: tool.description,
|
|
4201
|
+
name: tool.name,
|
|
4202
|
+
parameters: toGeminiSchema(tool.parameters ?? {
|
|
4203
|
+
additionalProperties: true,
|
|
4204
|
+
type: "object"
|
|
4205
|
+
})
|
|
4206
|
+
}))
|
|
4207
|
+
}
|
|
4208
|
+
] : undefined
|
|
4209
|
+
}),
|
|
4210
|
+
headers: {
|
|
4211
|
+
"content-type": "application/json"
|
|
4212
|
+
},
|
|
4213
|
+
method: "POST"
|
|
4214
|
+
});
|
|
4215
|
+
if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
|
|
4216
|
+
break;
|
|
4217
|
+
}
|
|
4218
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
4219
|
+
await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
|
|
4220
|
+
}
|
|
4221
|
+
if (!response) {
|
|
4222
|
+
throw new Error("Gemini voice assistant model failed: no response");
|
|
4223
|
+
}
|
|
4224
|
+
if (!response.ok) {
|
|
4225
|
+
throw createHTTPError("Gemini", response);
|
|
4226
|
+
}
|
|
4227
|
+
const body = await response.json();
|
|
4228
|
+
if (body.usageMetadata && typeof body.usageMetadata === "object") {
|
|
4229
|
+
await options.onUsage?.(body.usageMetadata);
|
|
4230
|
+
}
|
|
4231
|
+
const toolCalls = extractGeminiToolCalls(body);
|
|
4232
|
+
if (toolCalls.length) {
|
|
4233
|
+
return {
|
|
4234
|
+
assistantText: extractGeminiText(body) || undefined,
|
|
4235
|
+
toolCalls
|
|
4236
|
+
};
|
|
4237
|
+
}
|
|
4238
|
+
return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
};
|
|
4242
|
+
|
|
3471
4243
|
// src/store.ts
|
|
3472
4244
|
var createId = () => crypto.randomUUID();
|
|
3473
4245
|
var createVoiceSessionRecord = (id, scenarioId) => ({
|
|
@@ -3508,6 +4280,111 @@ var toVoiceSessionSummary = (session) => ({
|
|
|
3508
4280
|
turnCount: session.turns.length
|
|
3509
4281
|
});
|
|
3510
4282
|
|
|
4283
|
+
// src/testing/providerSimulator.ts
|
|
4284
|
+
var getContextQuery = (context) => context.query;
|
|
4285
|
+
var titleCaseProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
|
|
4286
|
+
var resolveRequestedProvider = (context, providers) => {
|
|
4287
|
+
const provider = getContextQuery(context).provider;
|
|
4288
|
+
return providers.includes(provider) ? provider : providers[0];
|
|
4289
|
+
};
|
|
4290
|
+
var createVoiceProviderFailureSimulator = (options) => {
|
|
4291
|
+
if (options.providers.length === 0) {
|
|
4292
|
+
throw new Error("At least one provider is required.");
|
|
4293
|
+
}
|
|
4294
|
+
const providerModels = Object.fromEntries(options.providers.map((provider) => [
|
|
4295
|
+
provider,
|
|
4296
|
+
{
|
|
4297
|
+
generate: async (input) => {
|
|
4298
|
+
const query = getContextQuery(input.context);
|
|
4299
|
+
if (provider === query.simulateFailureProvider) {
|
|
4300
|
+
const label = options.providerLabel?.(provider) ?? titleCaseProvider(provider);
|
|
4301
|
+
throw new Error(`${label} voice assistant model failed: HTTP 429`);
|
|
4302
|
+
}
|
|
4303
|
+
if (options.response) {
|
|
4304
|
+
return options.response({
|
|
4305
|
+
...input,
|
|
4306
|
+
mode: query.recoverProvider === provider ? "recovery" : "failure",
|
|
4307
|
+
provider
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
4310
|
+
return {
|
|
4311
|
+
assistantText: `Simulated ${provider} provider recovered.`
|
|
4312
|
+
};
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
]));
|
|
4316
|
+
const router = createVoiceProviderRouter({
|
|
4317
|
+
allowProviders: async (input) => {
|
|
4318
|
+
const recoverProvider = getContextQuery(input.context).recoverProvider;
|
|
4319
|
+
if (recoverProvider) {
|
|
4320
|
+
return [recoverProvider];
|
|
4321
|
+
}
|
|
4322
|
+
if (typeof options.allowProviders === "function") {
|
|
4323
|
+
return options.allowProviders(input);
|
|
4324
|
+
}
|
|
4325
|
+
return options.allowProviders ?? options.providers;
|
|
4326
|
+
},
|
|
4327
|
+
fallback: async (input) => {
|
|
4328
|
+
const selectedProvider = resolveRequestedProvider(input.context, options.providers);
|
|
4329
|
+
if (typeof options.fallback === "function") {
|
|
4330
|
+
return options.fallback(selectedProvider, input);
|
|
4331
|
+
}
|
|
4332
|
+
return options.fallback ?? options.providers.filter((provider) => provider !== selectedProvider);
|
|
4333
|
+
},
|
|
4334
|
+
fallbackMode: "provider-error",
|
|
4335
|
+
isProviderError: options.isProviderError,
|
|
4336
|
+
isRateLimitError: options.isRateLimitError,
|
|
4337
|
+
onProviderEvent: options.onProviderEvent,
|
|
4338
|
+
policy: "prefer-selected",
|
|
4339
|
+
providerHealth: options.providerHealth ?? {
|
|
4340
|
+
cooldownMs: 30000,
|
|
4341
|
+
failureThreshold: 1,
|
|
4342
|
+
rateLimitCooldownMs: 120000
|
|
4343
|
+
},
|
|
4344
|
+
providers: providerModels,
|
|
4345
|
+
selectProvider: ({ context }) => resolveRequestedProvider(context, options.providers)
|
|
4346
|
+
});
|
|
4347
|
+
const run = async (provider, mode) => {
|
|
4348
|
+
const now = Date.now();
|
|
4349
|
+
const session = createVoiceSessionRecord(`provider-sim-${now}`, "provider-simulation");
|
|
4350
|
+
const turn = {
|
|
4351
|
+
committedAt: now,
|
|
4352
|
+
id: `provider-sim-turn-${now}`,
|
|
4353
|
+
text: mode === "failure" ? `Simulate ${provider} provider failure.` : `Simulate ${provider} provider recovery.`,
|
|
4354
|
+
transcripts: []
|
|
4355
|
+
};
|
|
4356
|
+
const context = {
|
|
4357
|
+
query: {
|
|
4358
|
+
provider,
|
|
4359
|
+
...mode === "recovery" ? { recoverProvider: provider } : {},
|
|
4360
|
+
...mode === "failure" ? { simulateFailureProvider: provider } : {}
|
|
4361
|
+
}
|
|
4362
|
+
};
|
|
4363
|
+
const result = await router.generate({
|
|
4364
|
+
agentId: "provider-simulator",
|
|
4365
|
+
context,
|
|
4366
|
+
messages: [
|
|
4367
|
+
{
|
|
4368
|
+
content: turn.text,
|
|
4369
|
+
role: "user"
|
|
4370
|
+
}
|
|
4371
|
+
],
|
|
4372
|
+
session,
|
|
4373
|
+
system: "Simulate provider routing without calling external APIs.",
|
|
4374
|
+
tools: [],
|
|
4375
|
+
turn
|
|
4376
|
+
});
|
|
4377
|
+
return {
|
|
4378
|
+
mode,
|
|
4379
|
+
provider,
|
|
4380
|
+
result,
|
|
4381
|
+
status: "simulated"
|
|
4382
|
+
};
|
|
4383
|
+
};
|
|
4384
|
+
return {
|
|
4385
|
+
run
|
|
4386
|
+
};
|
|
4387
|
+
};
|
|
3511
4388
|
// src/memoryStore.ts
|
|
3512
4389
|
var createVoiceMemoryStore = () => {
|
|
3513
4390
|
const sessions = new Map;
|
|
@@ -7208,6 +8085,7 @@ export {
|
|
|
7208
8085
|
getDefaultVoiceDuplexBenchmarkScenarios,
|
|
7209
8086
|
getDefaultTTSBenchmarkFixtures,
|
|
7210
8087
|
evaluateSTTBenchmarkAcceptance,
|
|
8088
|
+
createVoiceProviderFailureSimulator,
|
|
7211
8089
|
createVoiceCallReviewRecorder,
|
|
7212
8090
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
7213
8091
|
createTelephonyVoiceTestFixtures,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { VoiceAgentModelInput, VoiceAgentModelOutput } from '../agent';
|
|
2
|
+
import type { VoiceSessionRecord } from '../types';
|
|
3
|
+
import type { VoiceProviderRouterEvent, VoiceProviderRouterHealthOptions } from '../modelAdapters';
|
|
4
|
+
export type VoiceProviderFailureSimulationMode = 'failure' | 'recovery';
|
|
5
|
+
export type VoiceProviderFailureSimulationContext<TProvider extends string = string> = {
|
|
6
|
+
query: {
|
|
7
|
+
provider: TProvider;
|
|
8
|
+
recoverProvider?: TProvider;
|
|
9
|
+
simulateFailureProvider?: TProvider;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type VoiceProviderFailureSimulationResult<TProvider extends string = string, TResult = unknown> = {
|
|
13
|
+
mode: VoiceProviderFailureSimulationMode;
|
|
14
|
+
provider: TProvider;
|
|
15
|
+
result: VoiceAgentModelOutput<TResult>;
|
|
16
|
+
status: 'simulated';
|
|
17
|
+
};
|
|
18
|
+
type ProviderListResolver<TContext, TSession extends VoiceSessionRecord, TProvider extends string> = readonly TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
|
|
19
|
+
export type VoiceProviderFailureSimulatorOptions<TContext extends VoiceProviderFailureSimulationContext<TProvider>, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown, TProvider extends string = string> = {
|
|
20
|
+
allowProviders?: ProviderListResolver<TContext, TSession, TProvider>;
|
|
21
|
+
fallback?: readonly TProvider[] | ((provider: TProvider, input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
|
|
22
|
+
isProviderError?: (error: unknown, provider: TProvider) => boolean;
|
|
23
|
+
isRateLimitError?: (error: unknown, provider: TProvider) => boolean;
|
|
24
|
+
onProviderEvent?: (event: VoiceProviderRouterEvent<TProvider>, input: VoiceAgentModelInput<TContext, TSession>) => Promise<void> | void;
|
|
25
|
+
providerHealth?: boolean | VoiceProviderRouterHealthOptions;
|
|
26
|
+
providerLabel?: (provider: TProvider) => string;
|
|
27
|
+
providers: readonly TProvider[];
|
|
28
|
+
response?: (input: {
|
|
29
|
+
mode: VoiceProviderFailureSimulationMode;
|
|
30
|
+
provider: TProvider;
|
|
31
|
+
} & VoiceAgentModelInput<TContext, TSession>) => VoiceAgentModelOutput<TResult> | Promise<VoiceAgentModelOutput<TResult>>;
|
|
32
|
+
};
|
|
33
|
+
export declare const createVoiceProviderFailureSimulator: <TProvider extends string, TResult = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TContext extends VoiceProviderFailureSimulationContext<TProvider> = VoiceProviderFailureSimulationContext<TProvider>>(options: VoiceProviderFailureSimulatorOptions<TContext, TSession, TResult, TProvider>) => {
|
|
34
|
+
run: (provider: TProvider, mode: VoiceProviderFailureSimulationMode) => Promise<VoiceProviderFailureSimulationResult<TProvider, TResult>>;
|
|
35
|
+
};
|
|
36
|
+
export {};
|