diff --git a/CHANGELOG.md b/CHANGELOG.md index 068d317f6e4..1172c239ae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai - Web search/Firecrawl: allow self-hosted private/internal Firecrawl `baseUrl` endpoints, including HTTP for private targets, while keeping hosted Firecrawl on the strict official endpoint. Fixes #63877 and supersedes #59666, #63941, and #74013. Thanks @jhthompson12, @jzakirov, @Mlightsnow, and @shad0wca7. - CLI/models: report gateway model fallback attempts in `infer model run --json` and avoid double-prefixing provider-qualified defaults such as `openrouter/auto` in `models status`. Partially fixes #69527. Thanks @alexifra. - Providers/OpenRouter: strip trailing assistant prefill turns from verified OpenRouter Anthropic model requests when reasoning is enabled, so Claude 4.6 routes no longer fail with Anthropic's prefill rejection through the OpenAI-compatible adapter. Fixes #75395. Thanks @sbmilburn. +- Voice Call: add per-number inbound routing for dialed-number greetings, response agents/models/prompts, and TTS voice overrides. Fixes #56604. Thanks @healthstatus. - Feishu: preserve Feishu/Lark HTTP error bodies for message sends, media sends, and chat member lookups, so HTTP 400 failures include vendor code, message, log id, and troubleshooter details. Fixes #73860. Thanks @desksk. - Agents/transcripts: avoid reopening large Pi transcript files through the synchronous session manager for maintenance rewrites, persisted tool-result truncation, manual compaction boundary hardening, and queued compaction rotation. Thanks @mariozechner. - Web search/Exa: accept `plugins.entries.exa.config.webSearch.baseUrl`, normalize it to the Exa `/search` endpoint, and partition cached results by endpoint. Fixes #54928 and supersedes #54939. Thanks @mrpl327 and @lyfuci. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index a14957d7ca7..39f05f1c2cf 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -a7158716d9262edba32ef9a18ab04d9f48f83cb903444b6f87b991977b6be52f config-baseline.json +366770fd037ace1092595b351fbd83473ee1ecce188bceb0ab4510a5579a9073 config-baseline.json 2d132b4c2e3b0e0f2524fc1cc889d3be658ad0e40c970b2d367bf27348883658 config-baseline.core.json f42329d45c095881bd226bdb192c235980658fd250606d0c0badc2b12f12f5d3 config-baseline.channel.json -de03faf42db470fe419a3f93a5777161f830f0355912603c6795945e42f39735 config-baseline.plugin.json +fffe0e74eab92a88c3c57952a70bc932438ce3a7f5f9982688437f2cdaee0bcb config-baseline.plugin.json diff --git a/docs/plugins/voice-call.md b/docs/plugins/voice-call.md index 27b5041de7c..55bfdd77859 100644 --- a/docs/plugins/voice-call.md +++ b/docs/plugins/voice-call.md @@ -110,6 +110,17 @@ Voice-call credentials accept SecretRefs. `plugins.entries.voice-call.config.twi fromNumber: "+15550001234", // or TWILIO_FROM_NUMBER for Twilio toNumber: "+15550005678", sessionScope: "per-phone", // per-phone | per-call + numbers: { + "+15550009999": { + inboundGreeting: "Silver Fox Cards, how can I help?", + responseSystemPrompt: "You are a concise baseball card specialist.", + tts: { + providers: { + openai: { voice: "alloy" }, + }, + }, + }, + }, twilio: { accountSid: "ACxxxxxxxx", @@ -500,6 +511,57 @@ identity. Auto-responses use the agent system. Tune with `responseModel`, `responseSystemPrompt`, and `responseTimeoutMs`. +### Per-number Routing + +Use `numbers` when one Voice Call plugin receives calls for multiple phone +numbers and each number should behave like a different line. For example, one +number can use a casual personal assistant while another uses a business +persona, a different response agent, and a different TTS voice. + +Routes are selected from the provider-supplied dialed `To` number. Keys must be +E.164 numbers. When a call arrives, Voice Call resolves the matching route once, +stores the matched route on the call record, and reuses that effective config +for the greeting, classic auto-response path, realtime consult path, and TTS +playback. If no route matches, the global Voice Call config is used. +Outbound calls do not use `numbers`; pass the outbound target, message, and +session explicitly when initiating the call. + +Route overrides currently support: + +- `inboundGreeting` +- `tts` +- `agentId` +- `responseModel` +- `responseSystemPrompt` +- `responseTimeoutMs` + +The `tts` route value deep-merges over the global Voice Call `tts` config, so +you can usually override only the provider voice: + +```json5 +{ + inboundGreeting: "Hello from the main line.", + responseSystemPrompt: "You are the default voice assistant.", + tts: { + provider: "openai", + providers: { + openai: { voice: "coral" }, + }, + }, + numbers: { + "+15550001111": { + inboundGreeting: "Silver Fox Cards, how can I help?", + responseSystemPrompt: "You are a concise baseball card specialist.", + tts: { + providers: { + openai: { voice: "alloy" }, + }, + }, + }, + }, +} +``` + ### Spoken output contract For auto-responses, Voice Call appends a strict spoken-output contract to diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index 1c4bc868ec4..0edd5779aae 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -42,6 +42,11 @@ const voiceCallConfigSchema = { inboundPolicy: { label: "Inbound Policy" }, allowFrom: { label: "Inbound Allowlist" }, inboundGreeting: { label: "Inbound Greeting", advanced: true }, + numbers: { + label: "Per-number Routing", + help: "Inbound overrides keyed by dialed E.164 number.", + advanced: true, + }, "telnyx.apiKey": { label: "Telnyx API Key", sensitive: true }, "telnyx.connectionId": { label: "Telnyx Connection ID" }, "telnyx.publicKey": { label: "Telnyx Public Key", sensitive: true }, diff --git a/extensions/voice-call/openclaw.plugin.json b/extensions/voice-call/openclaw.plugin.json index fa37e35bb28..1ac33e6e594 100644 --- a/extensions/voice-call/openclaw.plugin.json +++ b/extensions/voice-call/openclaw.plugin.json @@ -45,6 +45,11 @@ "label": "Inbound Greeting", "advanced": true }, + "numbers": { + "label": "Per-number Routing", + "help": "Inbound overrides keyed by dialed E.164 number.", + "advanced": true + }, "telnyx.apiKey": { "label": "Telnyx API Key", "sensitive": true @@ -279,6 +284,38 @@ "inboundGreeting": { "type": "string" }, + "numbers": { + "type": "object", + "propertyNames": { + "pattern": "^\\+[1-9]\\d{1,14}$" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "inboundGreeting": { + "type": "string" + }, + "tts": { + "$ref": "#/properties/tts" + }, + "agentId": { + "type": "string", + "minLength": 1 + }, + "responseModel": { + "type": "string" + }, + "responseSystemPrompt": { + "type": "string" + }, + "responseTimeoutMs": { + "type": "integer", + "minimum": 1 + } + } + } + }, "outbound": { "type": "object", "additionalProperties": false, diff --git a/extensions/voice-call/src/config.test.ts b/extensions/voice-call/src/config.test.ts index 6a699daef00..3985589692f 100644 --- a/extensions/voice-call/src/config.test.ts +++ b/extensions/voice-call/src/config.test.ts @@ -2,6 +2,8 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { VoiceCallConfigSchema, resolveTwilioAuthToken, + resolveVoiceCallEffectiveConfig, + resolveVoiceCallNumberRouteKey, resolveVoiceCallSessionKey, validateProviderConfig, normalizeVoiceCallConfig, @@ -304,6 +306,69 @@ describe("resolveVoiceCallConfig", () => { }), ).toBe("meet-room-1"); }); + + it("resolves per-number inbound route overrides over global voice settings", () => { + const config = resolveVoiceCallConfig({ + enabled: true, + provider: "mock", + inboundGreeting: "Hello from global.", + agentId: "main", + responseModel: "openai/gpt-5.4-mini", + responseSystemPrompt: "Global voice assistant.", + responseTimeoutMs: 10000, + tts: { + provider: "openai", + providers: { + openai: { voice: "coral", speed: 1 }, + }, + }, + numbers: { + "+15550001111": { + inboundGreeting: "Silver Fox Cards, how can I help?", + agentId: "cards", + responseModel: "openai/gpt-5.5", + responseSystemPrompt: "You are a baseball card expert.", + responseTimeoutMs: 20000, + tts: { + providers: { + openai: { voice: "alloy" }, + }, + }, + }, + }, + }); + + expect(resolveVoiceCallNumberRouteKey(config, "+1 (555) 000-1111")).toBe("+15550001111"); + const effective = resolveVoiceCallEffectiveConfig(config, "+1 (555) 000-1111"); + + expect(effective.numberRouteKey).toBe("+15550001111"); + expect(effective.config.inboundGreeting).toBe("Silver Fox Cards, how can I help?"); + expect(effective.config.agentId).toBe("cards"); + expect(effective.config.responseModel).toBe("openai/gpt-5.5"); + expect(effective.config.responseSystemPrompt).toBe("You are a baseball card expert."); + expect(effective.config.responseTimeoutMs).toBe(20000); + expect(effective.config.tts?.provider).toBe("openai"); + expect(effective.config.tts?.providers?.openai).toEqual({ voice: "alloy", speed: 1 }); + }); + + it("falls back to global voice settings when no per-number route matches", () => { + const config = resolveVoiceCallConfig({ + enabled: true, + provider: "mock", + inboundGreeting: "Hello from global.", + numbers: { + "+15550001111": { + inboundGreeting: "Hello from route.", + }, + }, + }); + + const effective = resolveVoiceCallEffectiveConfig(config, "+15550002222"); + + expect(effective.numberRouteKey).toBeUndefined(); + expect(effective.config).toBe(config); + expect(effective.config.inboundGreeting).toBe("Hello from global."); + }); }); describe("normalizeVoiceCallConfig", () => { diff --git a/extensions/voice-call/src/config.ts b/extensions/voice-call/src/config.ts index 32596dbff8e..dff394ee752 100644 --- a/extensions/voice-call/src/config.ts +++ b/extensions/voice-call/src/config.ts @@ -74,6 +74,24 @@ export type PlivoConfig = z.infer; export type VoiceCallTtsConfig = z.infer; +const VoiceCallNumberRouteConfigSchema = z + .object({ + /** Greeting message for inbound calls to this number. */ + inboundGreeting: z.string().optional(), + /** TTS override for inbound calls to this number. Deep-merges with global voice-call TTS. */ + tts: TtsConfigSchema, + /** Agent ID to use for voice response generation for this number. */ + agentId: z.string().min(1).optional(), + /** Optional model override for voice responses for this number. */ + responseModel: z.string().optional(), + /** System prompt for voice responses for this number. */ + responseSystemPrompt: z.string().optional(), + /** Timeout for response generation in ms for this number. */ + responseTimeoutMs: z.number().int().positive().optional(), + }) + .strict(); +export type VoiceCallNumberRouteConfig = z.infer; + // ----------------------------------------------------------------------------- // Webhook Server Configuration // ----------------------------------------------------------------------------- @@ -353,6 +371,9 @@ export const VoiceCallConfigSchema = z /** Greeting message for inbound calls */ inboundGreeting: z.string().optional(), + /** Per-dialed-number overrides for inbound calls. Keys are E.164 numbers. */ + numbers: z.record(E164Schema, VoiceCallNumberRouteConfigSchema).default({}), + /** Outbound call configuration */ outbound: OutboundConfigSchema, @@ -426,6 +447,10 @@ export const VoiceCallConfigSchema = z .strict(); export type VoiceCallConfig = z.infer; +export type VoiceCallEffectiveConfigResult = { + config: VoiceCallConfig; + numberRouteKey?: string; +}; type DeepPartial = T extends SecretInput ? T : T extends Array @@ -480,6 +505,56 @@ function normalizeVoiceCallTtsConfig( return TtsConfigSchema.parse(deepMergeDefined(defaults ?? {}, overrides ?? {})); } +function normalizePhoneRouteKey(phone: string | undefined): string { + return phone?.replace(/\D/g, "") ?? ""; +} + +export function resolveVoiceCallNumberRouteKey( + config: Pick, + phone: string | undefined, +): string | undefined { + const routes = config.numbers; + if (!routes) { + return undefined; + } + if (phone && Object.prototype.hasOwnProperty.call(routes, phone)) { + return phone; + } + + const normalizedPhone = normalizePhoneRouteKey(phone); + if (!normalizedPhone) { + return undefined; + } + return Object.keys(routes).find( + (routeKey) => normalizePhoneRouteKey(routeKey) === normalizedPhone, + ); +} + +export function resolveVoiceCallEffectiveConfig( + config: VoiceCallConfig, + phoneOrRouteKey: string | undefined, +): VoiceCallEffectiveConfigResult { + const numberRouteKey = resolveVoiceCallNumberRouteKey(config, phoneOrRouteKey); + if (!numberRouteKey) { + return { config }; + } + + const route = config.numbers[numberRouteKey]; + if (!route) { + return { config }; + } + + return { + numberRouteKey, + config: { + ...config, + ...route, + tts: normalizeVoiceCallTtsConfig(config.tts, route.tts), + numbers: config.numbers, + }, + }; +} + function sanitizeVoiceCallProviderConfigs( value: Record | undefined> | undefined, ): Record> { @@ -493,6 +568,19 @@ function sanitizeVoiceCallProviderConfigs( ); } +function sanitizeVoiceCallNumberRoutes( + value: Record | undefined, +): Record { + if (!value) { + return {}; + } + return Object.fromEntries( + Object.entries(value) + .filter((entry): entry is [string, unknown] => entry[1] !== undefined) + .map(([key, route]) => [key, VoiceCallNumberRouteConfigSchema.parse(route)]), + ); +} + export function resolveTwilioAuthToken( config: Pick, ): string | undefined { @@ -522,6 +610,9 @@ export function normalizeVoiceCallConfig(config: VoiceCallConfigInput): VoiceCal ...defaults, ...config, allowFrom: config.allowFrom ?? defaults.allowFrom, + numbers: sanitizeVoiceCallNumberRoutes( + (config.numbers ?? defaults.numbers) as Record, + ), outbound: { ...defaults.outbound, ...config.outbound }, serve, tailscale: { ...defaults.tailscale, ...config.tailscale }, diff --git a/extensions/voice-call/src/manager/events.test.ts b/extensions/voice-call/src/manager/events.test.ts index 41462817d09..2428e1b8b41 100644 --- a/extensions/voice-call/src/manager/events.test.ts +++ b/extensions/voice-call/src/manager/events.test.ts @@ -453,6 +453,43 @@ describe("processEvent (functional)", () => { expect(call.sessionKey).toBe(`voice:call:${call.callId}`); }); + it("applies per-number inbound greeting and stores the matched route key", () => { + const ctx = createContext({ + config: VoiceCallConfigSchema.parse({ + enabled: true, + provider: "plivo", + fromNumber: "+15550000000", + inboundPolicy: "open", + inboundGreeting: "Hello from global.", + numbers: { + "+15550002222": { + inboundGreeting: "Silver Fox Cards, how can I help?", + }, + }, + }), + }); + const event: NormalizedEvent = { + id: "evt-inbound-number-route", + type: "call.initiated", + callId: "CA-inbound-number-route", + providerCallId: "CA-inbound-number-route", + timestamp: Date.now(), + direction: "inbound", + from: "+15554444444", + to: "+1 (555) 000-2222", + }; + + processEvent(ctx, event); + + const call = requireFirstActiveCall(ctx); + expect(call.metadata).toEqual( + expect.objectContaining({ + initialMessage: "Silver Fox Cards, how can I help?", + numberRouteKey: "+15550002222", + }), + ); + }); + it("deduplicates by dedupeKey even when event IDs differ", () => { const now = Date.now(); const ctx = createContext(); diff --git a/extensions/voice-call/src/manager/events.ts b/extensions/voice-call/src/manager/events.ts index f2eea043239..3cbc96d8f19 100644 --- a/extensions/voice-call/src/manager/events.ts +++ b/extensions/voice-call/src/manager/events.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { isAllowlistedCaller, normalizePhoneNumber } from "../allowlist.js"; -import { resolveVoiceCallSessionKey } from "../config.js"; +import { resolveVoiceCallEffectiveConfig, resolveVoiceCallSessionKey } from "../config.js"; import type { CallRecord, NormalizedEvent } from "../types.js"; import type { CallManagerContext } from "./context.js"; import { finalizeCall } from "./lifecycle.js"; @@ -65,6 +65,11 @@ function createWebhookCall(params: { to: string; }): CallRecord { const callId = crypto.randomUUID(); + const effective = resolveVoiceCallEffectiveConfig( + params.ctx.config, + params.direction === "inbound" ? params.to : undefined, + ); + const effectiveConfig = effective.config; const callRecord: CallRecord = { callId, @@ -75,7 +80,7 @@ function createWebhookCall(params: { from: params.from, to: params.to, sessionKey: resolveVoiceCallSessionKey({ - config: params.ctx.config, + config: effectiveConfig, callId, phone: params.direction === "outbound" ? params.to : params.from, }), @@ -85,8 +90,9 @@ function createWebhookCall(params: { metadata: { initialMessage: params.direction === "inbound" - ? params.ctx.config.inboundGreeting || "Hello! How can I help you today?" + ? effectiveConfig.inboundGreeting || "Hello! How can I help you today?" : undefined, + ...(effective.numberRouteKey ? { numberRouteKey: effective.numberRouteKey } : {}), }, }; diff --git a/extensions/voice-call/src/manager/outbound.test.ts b/extensions/voice-call/src/manager/outbound.test.ts index 27077e0fb5d..436f4f8ec0e 100644 --- a/extensions/voice-call/src/manager/outbound.test.ts +++ b/extensions/voice-call/src/manager/outbound.test.ts @@ -361,6 +361,44 @@ describe("voice-call outbound helpers", () => { }); }); + it("uses per-number route TTS voice for routed inbound calls", async () => { + const call = { + callId: "call-1", + providerCallId: "provider-1", + state: "active", + to: "+15550002222", + metadata: { numberRouteKey: "+15550002222" }, + }; + const playTts = vi.fn(async () => {}); + const ctx = { + activeCalls: new Map([["call-1", call]]), + providerCallIdMap: new Map(), + provider: { name: "twilio", playTts }, + config: { + tts: { provider: "openai", providers: { openai: { voice: "coral" } } }, + numbers: { + "+15550002222": { + tts: { + providers: { + openai: { voice: "alloy" }, + }, + }, + }, + }, + }, + storePath: "/tmp/voice-call.json", + }; + + await expect(speak(ctx as never, "call-1", "hello")).resolves.toEqual({ success: true }); + + expect(playTts).toHaveBeenCalledWith({ + callId: "call-1", + providerCallId: "provider-1", + text: "hello", + voice: "alloy", + }); + }); + it("sends DTMF through connected provider calls", async () => { const call = { callId: "call-1", providerCallId: "provider-1", state: "active" }; const sendDtmfProvider = vi.fn(async () => {}); diff --git a/extensions/voice-call/src/manager/outbound.ts b/extensions/voice-call/src/manager/outbound.ts index 01a1218c8f0..8d94b09fa2a 100644 --- a/extensions/voice-call/src/manager/outbound.ts +++ b/extensions/voice-call/src/manager/outbound.ts @@ -1,6 +1,10 @@ import crypto from "node:crypto"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; -import { resolveVoiceCallSessionKey, type CallMode } from "../config.js"; +import { + resolveVoiceCallEffectiveConfig, + resolveVoiceCallSessionKey, + type CallMode, +} from "../config.js"; import { resolvePreferredTtsVoice } from "../tts-provider-voice.js"; import { type EndReason, @@ -242,7 +246,11 @@ export async function speak( transitionState(call, "speaking"); persistCallRecord(ctx.storePath, call); - const voice = resolvePreferredTtsVoice(ctx.config); + const numberRouteKey = + typeof call.metadata?.numberRouteKey === "string" ? call.metadata.numberRouteKey : call.to; + const voice = resolvePreferredTtsVoice( + resolveVoiceCallEffectiveConfig(ctx.config, numberRouteKey).config, + ); await provider.playTts({ callId, providerCallId, diff --git a/extensions/voice-call/src/runtime.test.ts b/extensions/voice-call/src/runtime.test.ts index 73a72fc0166..a2fd56f8a28 100644 --- a/extensions/voice-call/src/runtime.test.ts +++ b/extensions/voice-call/src/runtime.test.ts @@ -44,6 +44,7 @@ vi.mock("./config.js", () => ({ const normalizedPhone = params.phone?.replace(/\D/g, ""); return normalizedPhone ? `voice:${normalizedPhone}` : `voice:${params.callId}`; }, + resolveVoiceCallEffectiveConfig: (config: VoiceCallConfig) => ({ config }), resolveVoiceCallConfig: mocks.resolveVoiceCallConfig, resolveTwilioAuthToken: mocks.resolveTwilioAuthToken, validateProviderConfig: mocks.validateProviderConfig, diff --git a/extensions/voice-call/src/runtime.ts b/extensions/voice-call/src/runtime.ts index b5cb725b173..a1187396578 100644 --- a/extensions/voice-call/src/runtime.ts +++ b/extensions/voice-call/src/runtime.ts @@ -10,6 +10,7 @@ import { } from "openclaw/plugin-sdk/realtime-voice"; import type { VoiceCallConfig } from "./config.js"; import { + resolveVoiceCallEffectiveConfig, resolveVoiceCallSessionKey, resolveTwilioAuthToken, resolveVoiceCallConfig, @@ -339,13 +340,21 @@ export async function createVoiceCallRuntime(params: { if (!call) { return { error: `Call "${callId}" not found` }; } - const agentId = config.agentId ?? "main"; - const sessionKey = resolveVoiceCallConsultSessionKey({ ...call, config }); + const numberRouteKey = + typeof call.metadata?.numberRouteKey === "string" + ? call.metadata.numberRouteKey + : call.to; + const effectiveConfig = resolveVoiceCallEffectiveConfig(config, numberRouteKey).config; + const agentId = effectiveConfig.agentId ?? "main"; + const sessionKey = resolveVoiceCallConsultSessionKey({ + ...call, + config: effectiveConfig, + }); const fastContext = await resolveRealtimeFastContextConsult({ cfg, agentId, sessionKey, - config: config.realtime.fastContext, + config: effectiveConfig.realtime.fastContext, args, logger: log, }); @@ -353,7 +362,7 @@ export async function createVoiceCallRuntime(params: { return fastContext.result; } const { provider: agentProvider, model } = resolveVoiceResponseModel({ - voiceConfig: config, + voiceConfig: effectiveConfig, agentRuntime, }); const thinkLevel = agentRuntime.resolveThinkingDefault({ @@ -379,8 +388,10 @@ export async function createVoiceCallRuntime(params: { provider: agentProvider, model, thinkLevel, - timeoutMs: config.responseTimeoutMs, - toolsAllow: resolveRealtimeVoiceAgentConsultToolsAllow(config.realtime.toolPolicy), + timeoutMs: effectiveConfig.responseTimeoutMs, + toolsAllow: resolveRealtimeVoiceAgentConsultToolsAllow( + effectiveConfig.realtime.toolPolicy, + ), extraSystemPrompt: REALTIME_VOICE_CONSULT_SYSTEM_PROMPT, }); }, diff --git a/extensions/voice-call/src/test-fixtures.ts b/extensions/voice-call/src/test-fixtures.ts index 3fe58e1561d..75034c34330 100644 --- a/extensions/voice-call/src/test-fixtures.ts +++ b/extensions/voice-call/src/test-fixtures.ts @@ -11,6 +11,7 @@ export function createVoiceCallBaseConfig(params?: { fromNumber: "+15550001234", inboundPolicy: "disabled", allowFrom: [], + numbers: {}, outbound: { defaultMode: "notify", notifyHangupDelaySec: 3 }, maxDurationSeconds: 300, staleCallReaperSeconds: 600, diff --git a/extensions/voice-call/src/webhook.ts b/extensions/voice-call/src/webhook.ts index 7ca9106b14e..6c99f97516b 100644 --- a/extensions/voice-call/src/webhook.ts +++ b/extensions/voice-call/src/webhook.ts @@ -13,7 +13,11 @@ import { requestBodyErrorToText, } from "../api.js"; import { isAllowlistedCaller, normalizePhoneNumber } from "./allowlist.js"; -import { normalizeVoiceCallConfig, type VoiceCallConfig } from "./config.js"; +import { + normalizeVoiceCallConfig, + resolveVoiceCallEffectiveConfig, + type VoiceCallConfig, +} from "./config.js"; import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js"; import { getHeader } from "./http-headers.js"; import type { CallManager } from "./manager.js"; @@ -873,9 +877,12 @@ export class VoiceCallWebhookServer { try { const { generateVoiceResponse } = await loadResponseGeneratorModule(); + const numberRouteKey = + typeof call.metadata?.numberRouteKey === "string" ? call.metadata.numberRouteKey : call.to; + const effectiveConfig = resolveVoiceCallEffectiveConfig(this.config, numberRouteKey).config; const result = await generateVoiceResponse({ - voiceConfig: this.config, + voiceConfig: effectiveConfig, coreConfig: this.coreConfig, agentRuntime: this.agentRuntime, callId,