supafone-labs 0.4.13 → 0.5.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/src/index.ts CHANGED
@@ -235,6 +235,14 @@ export interface LabsVoiceSelection {
235
235
  model?: string;
236
236
  }
237
237
 
238
+ /** Public Agent Factory preference for one routed language. */
239
+ export interface LabsLanguageVoiceProfile {
240
+ language: string;
241
+ languageHint?: string;
242
+ language_hint?: string;
243
+ voice?: LabsVoiceSelection;
244
+ }
245
+
238
246
  export type LabsVoiceGender = "female" | "male" | "neutral";
239
247
 
240
248
  export interface LabsVoiceLanguage {
@@ -654,6 +662,192 @@ export interface LabsUltravoxRuntime {
654
662
  [extra: string]: unknown;
655
663
  }
656
664
 
665
+ export interface OutboundCallModeObservability {
666
+ enabled?: boolean;
667
+ includeTrigger?: boolean;
668
+ includeTransitions?: boolean;
669
+ includeTerminationReason?: boolean;
670
+ metadata?: Record<string, unknown>;
671
+ }
672
+
673
+ /** Public, provider-neutral controls for temporary outbound IVR navigation. */
674
+ export interface OutboundCallModeConfig {
675
+ enabled?: boolean;
676
+ autoDetect?: boolean;
677
+ dtmfToolEnabled?: boolean;
678
+ maxDurationSeconds?: number;
679
+ maxKeypresses?: number;
680
+ repeatedMenuLimit?: number;
681
+ noProgressTimeoutSeconds?: number;
682
+ humanDetectionEnabled?: boolean;
683
+ resumeOnHuman?: boolean;
684
+ observability?: OutboundCallModeObservability;
685
+ }
686
+
687
+ /** Capabilities positively reported by the active telephony/runtime adapter. */
688
+ export interface OutboundCallModeCapabilities {
689
+ provider?: string;
690
+ dtmf?: boolean;
691
+ statePersistence?: boolean;
692
+ humanDetection?: boolean;
693
+ observability?: boolean;
694
+ metadata?: Record<string, unknown>;
695
+ }
696
+
697
+ export interface OutboundCallModeReadiness {
698
+ ready: boolean;
699
+ status: "disabled" | "ready" | "unknown" | "unsupported";
700
+ provider: string;
701
+ transportFamily: string;
702
+ requiredCapabilities: string[];
703
+ missingCapabilities: string[];
704
+ unsupportedCapabilities: string[];
705
+ reasons: string[];
706
+ }
707
+
708
+ export const OUTBOUND_CALL_MODE_BOUNDS = {
709
+ maxDurationSeconds: { default: 180, min: 30, max: 900 },
710
+ maxKeypresses: { default: 12, min: 1, max: 64 },
711
+ repeatedMenuLimit: { default: 3, min: 1, max: 10 },
712
+ noProgressTimeoutSeconds: { default: 30, min: 5, max: 120 },
713
+ } as const;
714
+
715
+ export const OUTBOUND_CALL_MODE_DEFAULTS = {
716
+ enabled: false,
717
+ autoDetect: true,
718
+ dtmfToolEnabled: true,
719
+ maxDurationSeconds: 180,
720
+ maxKeypresses: 12,
721
+ repeatedMenuLimit: 3,
722
+ noProgressTimeoutSeconds: 30,
723
+ humanDetectionEnabled: true,
724
+ resumeOnHuman: true,
725
+ observability: {
726
+ enabled: true,
727
+ includeTrigger: true,
728
+ includeTransitions: true,
729
+ includeTerminationReason: true,
730
+ },
731
+ } as const;
732
+
733
+ export interface OutboundCallModeProviderProfile {
734
+ contractSupported: boolean;
735
+ execution: "adapter_runtime_dependent";
736
+ capabilityPolicy: "fail_closed";
737
+ transportFamily: string;
738
+ providers: readonly string[];
739
+ requiredCapabilities: readonly string[];
740
+ }
741
+
742
+ const OUTBOUND_PROVIDER_PROFILE_BASE = {
743
+ contractSupported: true,
744
+ execution: "adapter_runtime_dependent",
745
+ capabilityPolicy: "fail_closed",
746
+ requiredCapabilities: ["dtmf", "state_persistence", "human_detection"],
747
+ } as const;
748
+
749
+ export const OUTBOUND_CALL_MODE_PROVIDER_MATRIX: Record<
750
+ string,
751
+ OutboundCallModeProviderProfile
752
+ > = {
753
+ supafoneManaged: {
754
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
755
+ transportFamily: "supafone_managed",
756
+ providers: ["supafone", "supafone_managed", "managed"],
757
+ },
758
+ twilio: {
759
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
760
+ transportFamily: "twilio",
761
+ providers: ["twilio", "byo_twilio"],
762
+ },
763
+ telnyx: {
764
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
765
+ transportFamily: "telnyx",
766
+ providers: ["telnyx", "byo_telnyx"],
767
+ },
768
+ plivo: {
769
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
770
+ transportFamily: "plivo",
771
+ providers: ["plivo", "byo_plivo"],
772
+ },
773
+ signalWire: {
774
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
775
+ transportFamily: "signalwire",
776
+ providers: ["signalwire", "signal_wire", "byo_signalwire"],
777
+ },
778
+ sipByoc: {
779
+ ...OUTBOUND_PROVIDER_PROFILE_BASE,
780
+ transportFamily: "sip_byoc",
781
+ providers: ["sip", "custom_sip", "byo_sip", "byoc", "sip_byoc"],
782
+ },
783
+ };
784
+
785
+ export function outboundCallModeProviderProfile(provider: string): OutboundCallModeProviderProfile {
786
+ const normalized = String(provider ?? "").trim().toLowerCase();
787
+ for (const profile of Object.values(OUTBOUND_CALL_MODE_PROVIDER_MATRIX)) {
788
+ if (profile.providers.includes(normalized)) {
789
+ return { ...profile, providers: [...profile.providers] };
790
+ }
791
+ }
792
+ return {
793
+ contractSupported: false,
794
+ execution: "adapter_runtime_dependent",
795
+ capabilityPolicy: "fail_closed",
796
+ transportFamily: "unknown",
797
+ providers: normalized ? [normalized] : [],
798
+ requiredCapabilities: ["dtmf", "state_persistence", "human_detection"],
799
+ };
800
+ }
801
+
802
+ export function outboundCallModeReadiness(
803
+ config?: OutboundCallModeConfig,
804
+ capabilities?: OutboundCallModeCapabilities,
805
+ ): OutboundCallModeReadiness {
806
+ const provider = String(capabilities?.provider ?? "");
807
+ const profile = outboundCallModeProviderProfile(provider);
808
+ const normalized = outboundCallModePayload(config ?? {});
809
+ if (normalized.enabled !== true) {
810
+ return {
811
+ ready: true,
812
+ status: "disabled",
813
+ provider,
814
+ transportFamily: profile.transportFamily,
815
+ requiredCapabilities: [],
816
+ missingCapabilities: [],
817
+ unsupportedCapabilities: [],
818
+ reasons: [],
819
+ };
820
+ }
821
+
822
+ const requiredCapabilities = [...(normalized.required_capabilities as string[])];
823
+ const reported: Record<string, boolean | undefined> = {
824
+ dtmf: capabilities?.dtmf,
825
+ state_persistence: capabilities?.statePersistence,
826
+ human_detection: capabilities?.humanDetection,
827
+ observability: capabilities?.observability,
828
+ };
829
+ const missingCapabilities = requiredCapabilities.filter((key) => reported[key] === undefined);
830
+ const unsupportedCapabilities = requiredCapabilities.filter((key) => reported[key] === false);
831
+ const status = unsupportedCapabilities.length
832
+ ? "unsupported"
833
+ : missingCapabilities.length
834
+ ? "unknown"
835
+ : "ready";
836
+ const reasons = unsupportedCapabilities.length
837
+ ? unsupportedCapabilities.map((key) => `Adapter reports ${key} is unsupported.`)
838
+ : missingCapabilities.map((key) => `Adapter did not report ${key} support.`);
839
+ return {
840
+ ready: status === "ready",
841
+ status,
842
+ provider,
843
+ transportFamily: profile.transportFamily,
844
+ requiredCapabilities,
845
+ missingCapabilities,
846
+ unsupportedCapabilities,
847
+ reasons,
848
+ };
849
+ }
850
+
657
851
  export interface CreateLabsAgentRequest {
658
852
  agencyId?: string;
659
853
  agency_id?: string;
@@ -707,6 +901,15 @@ export interface CreateLabsAgentRequest {
707
901
  language?: string;
708
902
  preferredLanguage?: string;
709
903
  preferred_language?: string;
904
+ /** Opt in to managed live language and matching-voice routing. Defaults to false. */
905
+ languageVoiceRouting?: boolean;
906
+ language_voice_routing?: boolean;
907
+ /** Optional ordered language list. The first language controls the greeting. */
908
+ routingLanguages?: string[];
909
+ routing_languages?: string[];
910
+ /** Optional per-language voice preferences. Supports two to four profiles. */
911
+ languageProfiles?: LabsLanguageVoiceProfile[];
912
+ language_profiles?: LabsLanguageVoiceProfile[];
710
913
  voice?: LabsVoiceSelection;
711
914
  /** Resolve a real current catalog voice from this plain-language preference. */
712
915
  voicePreference?: LabsVoicePreference;
@@ -729,6 +932,9 @@ export interface CreateLabsAgentRequest {
729
932
  voice_watcher?: boolean;
730
933
  voiceWatcherModel?: string;
731
934
  voice_watcher_model?: string;
935
+ /** Opt-in outbound IVR mode. Execution depends on adapter-reported readiness. */
936
+ outboundCallMode?: OutboundCallModeConfig;
937
+ outbound_call_mode?: OutboundCallModeConfig;
732
938
  metadata?: Record<string, unknown>;
733
939
  }
734
940
 
@@ -755,6 +961,25 @@ export interface DeleteLabsAgentResponse {
755
961
  [extra: string]: unknown;
756
962
  }
757
963
 
964
+ export type LabsAgentLifecycle = "draft" | "active" | "paused" | "archived" | string;
965
+
966
+ export interface LabsAgentReadiness {
967
+ ready: boolean;
968
+ status?: string;
969
+ checks?: Array<Record<string, unknown>>;
970
+ [extra: string]: unknown;
971
+ }
972
+
973
+ export type UpdateLabsAgentRequest = Partial<CreateLabsAgentRequest>;
974
+
975
+ export interface LabsAgentLifecycleResponse {
976
+ success?: boolean;
977
+ agent?: LabsAgentResponse;
978
+ lifecycle?: LabsAgentLifecycle;
979
+ readiness?: LabsAgentReadiness;
980
+ [extra: string]: unknown;
981
+ }
982
+
758
983
  export interface LabsCapabilitiesResponse {
759
984
  product: string;
760
985
  api_namespace: string;
@@ -1134,6 +1359,8 @@ export interface LabsAgentResponse {
1134
1359
  display_name?: string;
1135
1360
  runtime_mode?: string;
1136
1361
  preset_key?: string;
1362
+ status?: string;
1363
+ lifecycle?: LabsAgentLifecycle;
1137
1364
  profile?: Record<string, unknown>;
1138
1365
  runtime?: Record<string, unknown>;
1139
1366
  [extra: string]: unknown;
@@ -1149,6 +1376,16 @@ export interface CreateLabsAgentResponse {
1149
1376
  [extra: string]: unknown;
1150
1377
  };
1151
1378
  call_plan?: LabsCallPlan;
1379
+ language_voice_routing?: {
1380
+ enabled: boolean;
1381
+ voice_routing_enabled: boolean;
1382
+ profiles: Array<Record<string, unknown>>;
1383
+ greeting_translation?: {
1384
+ language: string;
1385
+ language_hint: string;
1386
+ status: "not_needed" | "translated";
1387
+ };
1388
+ };
1152
1389
  [extra: string]: unknown;
1153
1390
  }
1154
1391
 
@@ -1159,6 +1396,9 @@ export interface ListLabsAgentsResponse {
1159
1396
 
1160
1397
  export interface GetLabsAgentResponse {
1161
1398
  agent: LabsAgentResponse;
1399
+ runtime?: Record<string, unknown>;
1400
+ widget?: Record<string, unknown>;
1401
+ readiness?: LabsAgentReadiness;
1162
1402
  }
1163
1403
 
1164
1404
  export interface STTResult {
@@ -2344,6 +2584,15 @@ export interface CampaignLiveView {
2344
2584
  stats?: Record<string, unknown> | null;
2345
2585
  }
2346
2586
 
2587
+ export interface CampaignCreateInput {
2588
+ name?: string;
2589
+ goal?: string;
2590
+ agentId?: string;
2591
+ accountId?: string;
2592
+ settings?: Record<string, unknown>;
2593
+ outboundCallMode?: OutboundCallModeConfig;
2594
+ }
2595
+
2347
2596
  export interface CampaignUpdateInput {
2348
2597
  name?: string;
2349
2598
  goal?: string;
@@ -2352,6 +2601,7 @@ export interface CampaignUpdateInput {
2352
2601
  emailBody?: string;
2353
2602
  cadence?: { channel: "voice" | "email"; delay_hours: number }[];
2354
2603
  settings?: Record<string, unknown>;
2604
+ outboundCallMode?: OutboundCallModeConfig;
2355
2605
  }
2356
2606
 
2357
2607
  export interface BrandScanResult {
@@ -2416,13 +2666,21 @@ class CampaignsNamespace {
2416
2666
  return this.sm.requestAccountApi("GET", `/api/v1/campaigns${query}`);
2417
2667
  }
2418
2668
 
2419
- create(opts: { name?: string; goal?: string; agentId?: string; accountId?: string } = {}): Promise<{ campaign: CampaignSummary }> {
2420
- return this.sm.requestAccountApi("POST", "/api/v1/campaigns", compact({
2669
+ async create(opts: CampaignCreateInput = {}): Promise<{ campaign: CampaignSummary }> {
2670
+ const created = await this.sm.requestAccountApi<{ campaign: CampaignSummary }>("POST", "/api/v1/campaigns", compact({
2421
2671
  name: opts.name ?? "New campaign",
2422
2672
  goal: opts.goal ?? "book",
2423
2673
  agent_id: opts.agentId,
2424
2674
  account_id: opts.accountId,
2425
2675
  }));
2676
+ const settings = campaignSettingsPayload(opts.settings, opts.outboundCallMode);
2677
+ if (!Object.keys(settings).length) return created;
2678
+ if (!created.campaign?.id) {
2679
+ throw new SupafoneLabsError(
2680
+ "Campaign was created but no campaign id was returned; outbound call mode was not saved.",
2681
+ );
2682
+ }
2683
+ return this.update(created.campaign.id, { settings });
2426
2684
  }
2427
2685
 
2428
2686
  get(campaignId: string): Promise<{ campaign: CampaignSummary }> {
@@ -2430,6 +2688,7 @@ class CampaignsNamespace {
2430
2688
  }
2431
2689
 
2432
2690
  update(campaignId: string, input: CampaignUpdateInput): Promise<{ campaign: CampaignSummary }> {
2691
+ const settings = campaignSettingsPayload(input.settings, input.outboundCallMode);
2433
2692
  const payload = compact({
2434
2693
  name: input.name,
2435
2694
  goal: input.goal,
@@ -2437,10 +2696,12 @@ class CampaignsNamespace {
2437
2696
  email_subject: input.emailSubject,
2438
2697
  email_body: input.emailBody,
2439
2698
  cadence: input.cadence,
2440
- settings: input.settings,
2699
+ settings: Object.keys(settings).length ? settings : undefined,
2441
2700
  });
2442
2701
  if (!Object.keys(payload as Record<string, unknown>).length) {
2443
- throw new SupafoneLabsError("Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, or settings");
2702
+ throw new SupafoneLabsError(
2703
+ "Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, settings, or outboundCallMode",
2704
+ );
2444
2705
  }
2445
2706
  return this.sm.requestAccountApi("PUT", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`, payload);
2446
2707
  }
@@ -2876,6 +3137,61 @@ class LabsAgentsNamespace {
2876
3137
  );
2877
3138
  }
2878
3139
 
3140
+ /** Update an existing durable agent without changing omitted fields. */
3141
+ update(
3142
+ agentKey: string,
3143
+ input: UpdateLabsAgentRequest,
3144
+ opts: GetLabsAgentOptions = {},
3145
+ ): Promise<LabsAgentLifecycleResponse> {
3146
+ const q = new URLSearchParams();
3147
+ if (opts.agencyId) q.set("agency_id", opts.agencyId);
3148
+ const suffix = q.toString() ? `?${q}` : "";
3149
+ return this.sm.requestSupafoneApi<LabsAgentLifecycleResponse>(
3150
+ "PATCH",
3151
+ `/api/v1/labs/agents/${encodeURIComponent(agentKey)}${suffix}`,
3152
+ labsAgentUpdatePayload(input),
3153
+ );
3154
+ }
3155
+
3156
+ /** Ask the managed server whether this agent is ready to activate. */
3157
+ readiness(agentKey: string, opts: GetLabsAgentOptions = {}): Promise<LabsAgentReadiness> {
3158
+ const q = new URLSearchParams();
3159
+ if (opts.agencyId) q.set("agency_id", opts.agencyId);
3160
+ const suffix = q.toString() ? `?${q}` : "";
3161
+ return this.sm.requestSupafoneApi<LabsAgentReadiness>(
3162
+ "GET",
3163
+ `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/readiness${suffix}`,
3164
+ );
3165
+ }
3166
+
3167
+ /** Activate a ready hosted agent. */
3168
+ activate(
3169
+ agentKey: string,
3170
+ opts: GetLabsAgentOptions = {},
3171
+ ): Promise<LabsAgentLifecycleResponse> {
3172
+ const q = new URLSearchParams();
3173
+ if (opts.agencyId) q.set("agency_id", opts.agencyId);
3174
+ const suffix = q.toString() ? `?${q}` : "";
3175
+ return this.sm.requestSupafoneApi<LabsAgentLifecycleResponse>(
3176
+ "POST",
3177
+ `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/activate${suffix}`,
3178
+ );
3179
+ }
3180
+
3181
+ /** Pause a hosted agent without deleting it. */
3182
+ pause(
3183
+ agentKey: string,
3184
+ opts: GetLabsAgentOptions = {},
3185
+ ): Promise<LabsAgentLifecycleResponse> {
3186
+ const q = new URLSearchParams();
3187
+ if (opts.agencyId) q.set("agency_id", opts.agencyId);
3188
+ const suffix = q.toString() ? `?${q}` : "";
3189
+ return this.sm.requestSupafoneApi<LabsAgentLifecycleResponse>(
3190
+ "POST",
3191
+ `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/pause${suffix}`,
3192
+ );
3193
+ }
3194
+
2879
3195
  /** Delete an agent. Optionally ask the backend to release assigned numbers. */
2880
3196
  delete(agentKey: string, opts: DeleteLabsAgentOptions = {}): Promise<DeleteLabsAgentResponse> {
2881
3197
  const q = new URLSearchParams();
@@ -3466,6 +3782,128 @@ class OptimizerNamespace {
3466
3782
  }
3467
3783
  }
3468
3784
 
3785
+ function outboundCallModePayload(config: OutboundCallModeConfig): Record<string, unknown> {
3786
+ const enabled = outboundModeBoolean(config.enabled, false, "enabled");
3787
+ if (!enabled) return { version: 1, enabled: false };
3788
+
3789
+ const observabilityInput = config.observability ?? {};
3790
+ if (typeof observabilityInput !== "object" || Array.isArray(observabilityInput)) {
3791
+ throw new SupafoneLabsError("outboundCallMode.observability must be an object");
3792
+ }
3793
+ if (
3794
+ observabilityInput.metadata !== undefined
3795
+ && (typeof observabilityInput.metadata !== "object"
3796
+ || observabilityInput.metadata === null
3797
+ || Array.isArray(observabilityInput.metadata))
3798
+ ) {
3799
+ throw new SupafoneLabsError("outboundCallMode.observability.metadata must be an object");
3800
+ }
3801
+ const observability = compact({
3802
+ enabled: outboundModeBoolean(observabilityInput.enabled, true, "observability.enabled"),
3803
+ include_trigger: outboundModeBoolean(
3804
+ observabilityInput.includeTrigger,
3805
+ true,
3806
+ "observability.includeTrigger",
3807
+ ),
3808
+ include_transitions: outboundModeBoolean(
3809
+ observabilityInput.includeTransitions,
3810
+ true,
3811
+ "observability.includeTransitions",
3812
+ ),
3813
+ include_termination_reason: outboundModeBoolean(
3814
+ observabilityInput.includeTerminationReason,
3815
+ true,
3816
+ "observability.includeTerminationReason",
3817
+ ),
3818
+ metadata: observabilityInput.metadata,
3819
+ });
3820
+ const payload: Record<string, unknown> = {
3821
+ version: 1,
3822
+ enabled: true,
3823
+ initial_mode: "mission",
3824
+ ivr_mode: "dynamic",
3825
+ transport_scope: "provider_agnostic",
3826
+ capability_policy: "fail_closed",
3827
+ auto_detect: outboundModeBoolean(config.autoDetect, true, "autoDetect"),
3828
+ dtmf_tool_enabled: outboundModeBoolean(config.dtmfToolEnabled, true, "dtmfToolEnabled"),
3829
+ max_duration_seconds: outboundModeInteger(
3830
+ config.maxDurationSeconds,
3831
+ "maxDurationSeconds",
3832
+ ),
3833
+ max_keypresses: outboundModeInteger(config.maxKeypresses, "maxKeypresses"),
3834
+ repeated_menu_limit: outboundModeInteger(config.repeatedMenuLimit, "repeatedMenuLimit"),
3835
+ no_progress_timeout_seconds: outboundModeInteger(
3836
+ config.noProgressTimeoutSeconds,
3837
+ "noProgressTimeoutSeconds",
3838
+ ),
3839
+ human_detection_enabled: outboundModeBoolean(
3840
+ config.humanDetectionEnabled,
3841
+ true,
3842
+ "humanDetectionEnabled",
3843
+ ),
3844
+ resume_on_human: outboundModeBoolean(config.resumeOnHuman, true, "resumeOnHuman"),
3845
+ observability,
3846
+ };
3847
+ payload.required_capabilities = outboundCallModeRequiredCapabilities(payload);
3848
+ return payload;
3849
+ }
3850
+
3851
+ function outboundModeBoolean(value: boolean | undefined, fallback: boolean, key: string): boolean {
3852
+ if (value === undefined) return fallback;
3853
+ if (typeof value !== "boolean") {
3854
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be a boolean`);
3855
+ }
3856
+ return value;
3857
+ }
3858
+
3859
+ function outboundModeInteger(
3860
+ value: number | undefined,
3861
+ key: keyof typeof OUTBOUND_CALL_MODE_BOUNDS,
3862
+ ): number {
3863
+ const bounds = OUTBOUND_CALL_MODE_BOUNDS[key];
3864
+ const normalized = value ?? bounds.default;
3865
+ if (!Number.isInteger(normalized)) {
3866
+ throw new SupafoneLabsError(`outboundCallMode.${key} must be an integer`);
3867
+ }
3868
+ if (normalized < bounds.min || normalized > bounds.max) {
3869
+ throw new SupafoneLabsError(
3870
+ `outboundCallMode.${key} must be between ${bounds.min} and ${bounds.max}`,
3871
+ );
3872
+ }
3873
+ return normalized;
3874
+ }
3875
+
3876
+ function outboundCallModeRequiredCapabilities(payload: Record<string, unknown>): string[] {
3877
+ const required = ["state_persistence"];
3878
+ if (payload.dtmf_tool_enabled !== false) required.unshift("dtmf");
3879
+ if (payload.human_detection_enabled !== false || payload.resume_on_human !== false) {
3880
+ required.push("human_detection");
3881
+ }
3882
+ const observability = payload.observability as Record<string, unknown> | undefined;
3883
+ if (observability?.enabled === true) required.push("observability");
3884
+ return required;
3885
+ }
3886
+
3887
+ function labsAgentMetadataPayload(input: CreateLabsAgentRequest): Record<string, unknown> | undefined {
3888
+ const metadata = { ...(input.metadata ?? {}) };
3889
+ const mode = input.outboundCallMode ?? input.outbound_call_mode;
3890
+ if (mode !== undefined) metadata.outbound_call_mode = outboundCallModePayload(mode);
3891
+ return Object.keys(metadata).length ? metadata : undefined;
3892
+ }
3893
+
3894
+ function labsAgentUpdatePayload(input: UpdateLabsAgentRequest): Record<string, unknown> {
3895
+ return labsAgentPayload(input as CreateLabsAgentRequest);
3896
+ }
3897
+
3898
+ function campaignSettingsPayload(
3899
+ settings?: Record<string, unknown>,
3900
+ mode?: OutboundCallModeConfig,
3901
+ ): Record<string, unknown> {
3902
+ const payload = { ...(settings ?? {}) };
3903
+ if (mode !== undefined) payload.outbound_call_mode = outboundCallModePayload(mode);
3904
+ return payload;
3905
+ }
3906
+
3469
3907
  function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown> {
3470
3908
  const fixedLanguage = input.preferred_language ?? input.preferredLanguage ?? input.language;
3471
3909
  return compact({
@@ -3494,6 +3932,9 @@ function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown
3494
3932
  greeting: input.greeting,
3495
3933
  system_prompt: input.system_prompt ?? input.systemPrompt,
3496
3934
  language: fixedLanguage,
3935
+ language_voice_routing: input.language_voice_routing ?? input.languageVoiceRouting,
3936
+ routing_languages: input.routing_languages ?? input.routingLanguages,
3937
+ language_profiles: languageProfilesPayload(input.language_profiles ?? input.languageProfiles),
3497
3938
  voice: input.voice ? voicePayload(input.voice) : undefined,
3498
3939
  voice_preference: voicePreferencePayload(
3499
3940
  input.voice_preference ?? input.voicePreference,
@@ -3512,7 +3953,7 @@ function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown
3512
3953
  ultravox: input.ultravox ? ultravoxPayload(input.ultravox) : undefined,
3513
3954
  voice_watcher: input.voice_watcher ?? input.voiceWatcher,
3514
3955
  voice_watcher_model: input.voice_watcher_model ?? input.voiceWatcherModel,
3515
- metadata: input.metadata,
3956
+ metadata: labsAgentMetadataPayload(input),
3516
3957
  });
3517
3958
  }
3518
3959
 
@@ -3761,6 +4202,18 @@ function voicePayload(input: LabsVoiceSelection): Record<string, unknown> {
3761
4202
  });
3762
4203
  }
3763
4204
 
4205
+ function languageProfilesPayload(
4206
+ input?: LabsLanguageVoiceProfile[],
4207
+ ): Record<string, unknown>[] | undefined {
4208
+ if (!Array.isArray(input)) return undefined;
4209
+ const profiles = input.map((profile) => compact({
4210
+ language: profile.language,
4211
+ language_hint: profile.language_hint ?? profile.languageHint,
4212
+ voice: profile.voice ? voicePayload(profile.voice) : undefined,
4213
+ }));
4214
+ return profiles.length ? profiles : undefined;
4215
+ }
4216
+
3764
4217
  function voicePreferencePayload(
3765
4218
  input?: LabsVoicePreference,
3766
4219
  defaultLanguage?: string,