From 3d1f5ffd542b0e656fce8e613db0cebb0379f721 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 12 Jul 2026 16:47:40 +0200 Subject: [PATCH] refactor(voice-call): retire runtime config compatibility (#105476) * refactor(voice-call): rename config migration module * refactor(voice-call): retire runtime config compatibility --- extensions/voice-call/README.md | 3 +- extensions/voice-call/config-api.ts | 12 - extensions/voice-call/index.test.ts | 61 +++-- extensions/voice-call/index.ts | 31 +-- extensions/voice-call/setup-api.ts | 2 +- .../voice-call/src/config-compat.test.ts | 210 ------------------ .../voice-call/src/config-migration.test.ts | 144 ++++++++++++ .../{config-compat.ts => config-migration.ts} | 119 +--------- 8 files changed, 185 insertions(+), 397 deletions(-) delete mode 100644 extensions/voice-call/config-api.ts delete mode 100644 extensions/voice-call/src/config-compat.test.ts create mode 100644 extensions/voice-call/src/config-migration.test.ts rename extensions/voice-call/src/{config-compat.ts => config-migration.ts} (56%) diff --git a/extensions/voice-call/README.md b/extensions/voice-call/README.md index 103f47007e2d..4e08253252a6 100644 --- a/extensions/voice-call/README.md +++ b/extensions/voice-call/README.md @@ -101,7 +101,7 @@ Notes: - Twilio defaults to US1. For a non-US Region, set `twilio.region` to `ie1` or `au1` and use credentials created in that Region; see [Twilio's regional REST API guide](https://www.twilio.com/docs/global-infrastructure/using-the-twilio-rest-api-in-a-non-us-region). - `mock` is a local dev provider (no network calls). - Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true. -- If older configs still use `provider: "log"`, `twilio.from`, or legacy `streaming.*` OpenAI keys, run `openclaw doctor --fix` to rewrite them. +- Runtime accepts canonical config only. If older configs still use `provider: "log"`, `twilio.from`, or legacy `streaming.*` OpenAI keys, run `openclaw doctor --fix` to rewrite them. - advanced webhook, streaming, and tunnel notes: `https://docs.openclaw.ai/plugins/voice-call` - `responseModel` is optional. When unset, voice responses use the runtime default model. - `sessionScope` defaults to `per-phone`, preserving caller memory across calls. Use `per-call` for reception, booking, IVR, and bridge flows where each carrier call should start fresh. @@ -163,4 +163,3 @@ Actions: - Outbound conversation calls suppress barge-in only while the initial greeting is actively speaking, then re-enable normal interruption. - Twilio stream disconnect auto-end uses a short grace window so quick reconnects do not end the call. - Realtime provider selection is generic. Configure `streaming.provider` / `realtime.provider` and put provider-owned options under `providers.`. -- Runtime fallback still accepts the old voice-call keys for now, but migration is a doctor step and the compat shim is scheduled to go away in a future release. diff --git a/extensions/voice-call/config-api.ts b/extensions/voice-call/config-api.ts deleted file mode 100644 index d4b64cdd12be..000000000000 --- a/extensions/voice-call/config-api.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Narrow barrel for config compatibility helpers consumed outside the plugin. -// Keep this separate from api.ts so config migration code does not pull in the -// full runtime-oriented voice-call surface. - -export { - VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION, - collectVoiceCallLegacyConfigIssues, - formatVoiceCallLegacyConfigWarnings, - migrateVoiceCallLegacyConfigInput, - normalizeVoiceCallLegacyConfigInput, - parseVoiceCallPluginConfig, -} from "./src/config-compat.js"; diff --git a/extensions/voice-call/index.test.ts b/extensions/voice-call/index.test.ts index 535cb2081aef..3025d2b47dfb 100644 --- a/extensions/voice-call/index.test.ts +++ b/extensions/voice-call/index.test.ts @@ -261,6 +261,33 @@ describe("voice-call plugin", () => { ]; }); + it("defaults canonical plugin config to an enabled mock runtime", async () => { + const { service } = setup({}); + + await service?.start(createServiceContext()); + + expect(createVoiceCallRuntime).toHaveBeenCalledTimes(1); + expect(firstRuntimeConfig()).toMatchObject({ + enabled: true, + provider: "mock", + }); + }); + + it.each([ + ["provider log", { enabled: true, provider: "log" }], + [ + "twilio.from", + { + enabled: true, + provider: "mock", + twilio: { from: "+15550001234" }, + }, + ], + ])("rejects legacy %s config instead of normalizing it at runtime", (_label, config) => { + expect(() => setup(config)).toThrow(); + expect(createVoiceCallRuntime).not.toHaveBeenCalled(); + }); + it("reuses a started runtime across plugin registration contexts", async () => { const first = setup({ provider: "mock" }); const second = setup({ provider: "mock" }); @@ -730,40 +757,6 @@ describe("voice-call plugin", () => { expect(error?.message).not.toContain("endedAt="); }); - it("normalizes legacy config through runtime creation and warns to run doctor", async () => { - const { methods } = setup({ - enabled: true, - provider: "log", - twilio: { - from: "+15550001234", - }, - streaming: { - enabled: true, - sttProvider: "openai", - openaiApiKey: "sk-test", // pragma: allowlist secret - }, - }); - const handler = methods.get("voicecall.status") as - | ((ctx: { - params: Record; - respond: ReturnType; - }) => Promise) - | undefined; - const respond = vi.fn(); - - await handler?.({ params: { callId: "call-1" }, respond }); - - expect(vi.mocked(createVoiceCallRuntime)).toHaveBeenCalledTimes(1); - const runtimeConfig = firstRuntimeConfig(); - expect(runtimeConfig?.enabled).toBe(true); - expect(runtimeConfig?.provider).toBe("mock"); - expect(runtimeConfig?.fromNumber).toBe("+15550001234"); - expect(runtimeConfig?.streaming?.enabled).toBe(true); - expect(runtimeConfig?.streaming?.provider).toBe("openai"); - expect(runtimeConfig?.streaming?.providers?.openai?.apiKey).toBe("sk-test"); - expectWarningIncludes('Run "openclaw doctor --fix"'); - }); - it("freezes the invoking agent on tool-created calls", async () => { const { tools } = setup({ provider: "mock" }, { agentId: "support" }); const tool = tools[0] as { diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index a8745723bbe1..e598d6822f0e 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -3,7 +3,10 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime"; import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime"; import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + asOptionalRecord, + normalizeOptionalString, +} from "openclaw/plugin-sdk/string-coerce-runtime"; import { jsonResult as json } from "openclaw/plugin-sdk/tool-results"; import { Type } from "typebox"; import { @@ -14,11 +17,7 @@ import { import { createVoiceCallRuntime, type VoiceCallRuntime } from "./runtime-entry.js"; import { registerVoiceCallCli } from "./src/cli.js"; import { - formatVoiceCallLegacyConfigWarnings, - normalizeVoiceCallLegacyConfigInput, - parseVoiceCallPluginConfig, -} from "./src/config-compat.js"; -import { + VoiceCallConfigSchema, resolveVoiceCallConfig, validateProviderConfig, type VoiceCallConfig, @@ -32,12 +31,12 @@ const VOICE_CALL_READ_METHOD_SCOPE = { scope: "operator.read" as const }; const voiceCallConfigSchema = { parse(value: unknown): VoiceCallConfig { - const normalized = normalizeVoiceCallLegacyConfigInput(value); - const enabled = typeof normalized.enabled === "boolean" ? normalized.enabled : true; - return parseVoiceCallPluginConfig({ - ...normalized, + const config = asOptionalRecord(value) ?? {}; + const enabled = typeof config.enabled === "boolean" ? config.enabled : true; + return VoiceCallConfigSchema.parse({ + ...config, enabled, - provider: normalized.provider ?? (enabled ? "mock" : undefined), + provider: config.provider ?? (enabled ? "mock" : undefined), }); }, uiHints: { @@ -290,16 +289,6 @@ export default definePluginEntry({ const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig)); const validation = validateProviderConfig(config); - if (api.pluginConfig && typeof api.pluginConfig === "object") { - for (const warning of formatVoiceCallLegacyConfigWarnings({ - value: api.pluginConfig, - configPathPrefix: "plugins.entries.voice-call.config", - doctorFixCommand: "openclaw doctor --fix", - })) { - api.logger.warn(warning); - } - } - const runtimeState = getVoiceCallRuntimeGlobalState(); const continueOperationStore = createVoiceCallContinueOperationStore({ config, diff --git a/extensions/voice-call/setup-api.ts b/extensions/voice-call/setup-api.ts index 24a4293505da..5db219ab1298 100644 --- a/extensions/voice-call/setup-api.ts +++ b/extensions/voice-call/setup-api.ts @@ -2,7 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { migrateVoiceCallLegacyConfigInput } from "./config-api.js"; +import { migrateVoiceCallLegacyConfigInput } from "./src/config-migration.js"; // Setup-time entrypoint for voice-call config migrations. diff --git a/extensions/voice-call/src/config-compat.test.ts b/extensions/voice-call/src/config-compat.test.ts deleted file mode 100644 index fc652fb804c5..000000000000 --- a/extensions/voice-call/src/config-compat.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -// Voice Call tests cover config compat plugin behavior. -import { describe, expect, it } from "vitest"; -import { - VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION, - collectVoiceCallLegacyConfigIssues, - formatVoiceCallLegacyConfigWarnings, - migrateVoiceCallLegacyConfigInput, - normalizeVoiceCallLegacyConfigInput, - parseVoiceCallPluginConfig, -} from "./config-compat.js"; - -describe("voice-call config compatibility", () => { - it("maps deprecated provider and twilio.from fields into canonical config", () => { - const parsed = parseVoiceCallPluginConfig({ - enabled: true, - provider: "log", - twilio: { - from: "+15550001234", - }, - }); - - expect(parsed.provider).toBe("mock"); - expect(parsed.fromNumber).toBe("+15550001234"); - }); - - it("moves legacy streaming OpenAI fields into streaming.providers.openai", () => { - const normalized = normalizeVoiceCallLegacyConfigInput({ - streaming: { - enabled: true, - sttProvider: "openai", - openaiApiKey: "sk-test", // pragma: allowlist secret - sttModel: "gpt-4o-transcribe", - silenceDurationMs: 700, - vadThreshold: 0.4, - }, - }); - - const streaming = normalized.streaming as - | { - enabled?: boolean; - provider?: string; - providers?: { - openai?: { - apiKey?: string; - model?: string; - silenceDurationMs?: number; - vadThreshold?: number; - }; - }; - openaiApiKey?: unknown; - sttModel?: unknown; - } - | undefined; - expect(streaming?.enabled).toBe(true); - expect(streaming?.provider).toBe("openai"); - expect(streaming?.providers?.openai).toEqual({ - apiKey: "sk-test", - model: "gpt-4o-transcribe", - silenceDurationMs: 700, - vadThreshold: 0.4, - }); - expect(streaming?.openaiApiKey).toBeUndefined(); - expect(streaming?.sttModel).toBeUndefined(); - }); - - it("removes legacy realtime agentContext system prompt toggle", () => { - const normalized = normalizeVoiceCallLegacyConfigInput({ - realtime: { - agentContext: { - enabled: true, - includeSystemPrompt: false, - includeWorkspaceFiles: true, - }, - }, - }); - - const agentContext = ( - normalized.realtime as - | { - agentContext?: { - enabled?: boolean; - includeSystemPrompt?: unknown; - includeWorkspaceFiles?: boolean; - }; - } - | undefined - )?.agentContext; - - expect(agentContext).toEqual({ - enabled: true, - includeWorkspaceFiles: true, - }); - }); - - it("does not migrate non-finite legacy streaming numbers", () => { - const migration = migrateVoiceCallLegacyConfigInput({ - value: { - streaming: { - silenceDurationMs: Number.NaN, - vadThreshold: Number.POSITIVE_INFINITY, - }, - }, - configPathPrefix: "plugins.entries.voice-call.config", - }); - const streaming = migration.config.streaming as - | { - providers?: { - openai?: { - silenceDurationMs?: number; - vadThreshold?: number; - }; - }; - } - | undefined; - - expect(streaming?.providers?.openai).toBeUndefined(); - expect(migration.changes).toEqual([ - "Removed invalid plugins.entries.voice-call.config.streaming.silenceDurationMs.", - "Removed invalid plugins.entries.voice-call.config.streaming.vadThreshold.", - ]); - expect(migration.issues.map((issue) => issue.path)).toEqual([ - "streaming.silenceDurationMs", - "streaming.vadThreshold", - ]); - }); - - it("reports doctor-oriented legacy issues and warnings", () => { - const raw = { - provider: "log", - twilio: { - from: "+15550001234", - }, - streaming: { - sttProvider: "openai", - openaiApiKey: "sk-test", // pragma: allowlist secret - }, - realtime: { - agentContext: { - includeSystemPrompt: true, - }, - }, - }; - - expect(collectVoiceCallLegacyConfigIssues(raw)).toEqual([ - { - path: "provider", - replacement: "provider", - message: 'Replace provider "log" with "mock".', - }, - { - path: "twilio.from", - replacement: "fromNumber", - message: "Move twilio.from to fromNumber.", - }, - { - path: "streaming.sttProvider", - replacement: "streaming.provider", - message: "Move streaming.sttProvider to streaming.provider.", - }, - { - path: "streaming.openaiApiKey", - replacement: "streaming.providers.openai.apiKey", - message: "Move streaming.openaiApiKey to streaming.providers.openai.apiKey.", - }, - { - path: "realtime.agentContext.includeSystemPrompt", - replacement: "realtime.agentContext", - message: - "Remove realtime.agentContext.includeSystemPrompt; realtime context now uses the generated agent prompt.", - }, - ]); - expect( - formatVoiceCallLegacyConfigWarnings({ - value: raw, - configPathPrefix: "plugins.entries.voice-call.config", - doctorFixCommand: "openclaw doctor --fix", - }), - ).toEqual([ - `[voice-call] legacy config keys detected under plugins.entries.voice-call.config; runtime loading will not rewrite them, and support for the legacy shape will be removed in ${VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION}. Run "openclaw doctor --fix".`, - '[voice-call] plugins.entries.voice-call.config.provider: Replace provider "log" with "mock".', - "[voice-call] plugins.entries.voice-call.config.twilio.from: Move twilio.from to fromNumber.", - "[voice-call] plugins.entries.voice-call.config.streaming.sttProvider: Move streaming.sttProvider to streaming.provider.", - "[voice-call] plugins.entries.voice-call.config.streaming.openaiApiKey: Move streaming.openaiApiKey to streaming.providers.openai.apiKey.", - "[voice-call] plugins.entries.voice-call.config.realtime.agentContext.includeSystemPrompt: Remove realtime.agentContext.includeSystemPrompt; realtime context now uses the generated agent prompt.", - ]); - }); - - it("returns doctor migration change lines", () => { - const migration = migrateVoiceCallLegacyConfigInput({ - value: { - provider: "log", - streaming: { - sttProvider: "openai", - }, - realtime: { - agentContext: { - includeSystemPrompt: true, - }, - }, - }, - configPathPrefix: "plugins.entries.voice-call.config", - }); - - expect(migration.changes).toEqual([ - 'Moved plugins.entries.voice-call.config.provider "log" → "mock".', - "Moved plugins.entries.voice-call.config.streaming.sttProvider → plugins.entries.voice-call.config.streaming.provider.", - "Removed plugins.entries.voice-call.config.realtime.agentContext.includeSystemPrompt.", - ]); - }); -}); diff --git a/extensions/voice-call/src/config-migration.test.ts b/extensions/voice-call/src/config-migration.test.ts new file mode 100644 index 000000000000..116acd2de9c3 --- /dev/null +++ b/extensions/voice-call/src/config-migration.test.ts @@ -0,0 +1,144 @@ +// Voice Call tests cover setup-time config migration behavior. +import { describe, expect, it } from "vitest"; +import { migrateVoiceCallLegacyConfigInput } from "./config-migration.js"; + +describe("voice-call config migration", () => { + it("maps deprecated provider and twilio.from fields into canonical config", () => { + const migration = migrateVoiceCallLegacyConfigInput({ + value: { + enabled: true, + provider: "log", + twilio: { + from: "+15550001234", + }, + }, + }); + + expect(migration.config.provider).toBe("mock"); + expect(migration.config.fromNumber).toBe("+15550001234"); + }); + + it("moves legacy streaming OpenAI fields into streaming.providers.openai", () => { + const migration = migrateVoiceCallLegacyConfigInput({ + value: { + streaming: { + enabled: true, + sttProvider: "openai", + openaiApiKey: "test", + sttModel: "gpt-4o-transcribe", + silenceDurationMs: 700, + vadThreshold: 0.4, + }, + }, + }); + + const streaming = migration.config.streaming as + | { + enabled?: boolean; + provider?: string; + providers?: { + openai?: { + apiKey?: string; + model?: string; + silenceDurationMs?: number; + vadThreshold?: number; + }; + }; + openaiApiKey?: unknown; + sttModel?: unknown; + } + | undefined; + expect(streaming?.enabled).toBe(true); + expect(streaming?.provider).toBe("openai"); + expect(streaming?.providers?.openai).toEqual({ + apiKey: "test", + model: "gpt-4o-transcribe", + silenceDurationMs: 700, + vadThreshold: 0.4, + }); + expect(streaming?.openaiApiKey).toBeUndefined(); + expect(streaming?.sttModel).toBeUndefined(); + }); + + it("removes legacy realtime agentContext system prompt toggle", () => { + const migration = migrateVoiceCallLegacyConfigInput({ + value: { + realtime: { + agentContext: { + enabled: true, + includeSystemPrompt: false, + includeWorkspaceFiles: true, + }, + }, + }, + }); + + const agentContext = ( + migration.config.realtime as + | { + agentContext?: { + enabled?: boolean; + includeSystemPrompt?: unknown; + includeWorkspaceFiles?: boolean; + }; + } + | undefined + )?.agentContext; + + expect(agentContext).toEqual({ + enabled: true, + includeWorkspaceFiles: true, + }); + }); + + it("does not migrate non-finite legacy streaming numbers", () => { + const migration = migrateVoiceCallLegacyConfigInput({ + value: { + streaming: { + silenceDurationMs: Number.NaN, + vadThreshold: Number.POSITIVE_INFINITY, + }, + }, + configPathPrefix: "plugins.entries.voice-call.config", + }); + const streaming = migration.config.streaming as + | { + providers?: { + openai?: { + silenceDurationMs?: number; + vadThreshold?: number; + }; + }; + } + | undefined; + + expect(streaming?.providers?.openai).toBeUndefined(); + expect(migration.changes).toEqual([ + "Removed invalid plugins.entries.voice-call.config.streaming.silenceDurationMs.", + "Removed invalid plugins.entries.voice-call.config.streaming.vadThreshold.", + ]); + }); + + it("returns doctor migration change lines", () => { + const migration = migrateVoiceCallLegacyConfigInput({ + value: { + provider: "log", + streaming: { + sttProvider: "openai", + }, + realtime: { + agentContext: { + includeSystemPrompt: true, + }, + }, + }, + configPathPrefix: "plugins.entries.voice-call.config", + }); + + expect(migration.changes).toEqual([ + 'Moved plugins.entries.voice-call.config.provider "log" → "mock".', + "Moved plugins.entries.voice-call.config.streaming.sttProvider → plugins.entries.voice-call.config.streaming.provider.", + "Removed plugins.entries.voice-call.config.realtime.agentContext.includeSystemPrompt.", + ]); + }); +}); diff --git a/extensions/voice-call/src/config-compat.ts b/extensions/voice-call/src/config-migration.ts similarity index 56% rename from extensions/voice-call/src/config-compat.ts rename to extensions/voice-call/src/config-migration.ts index 3755ab86736c..ad24803ea350 100644 --- a/extensions/voice-call/src/config-compat.ts +++ b/extensions/voice-call/src/config-migration.ts @@ -1,19 +1,5 @@ -// Voice Call helper module supports config compat behavior. +// Voice Call setup helper migrates legacy config to the canonical schema. import { asOptionalRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime"; -import type { VoiceCallConfig } from "./config.js"; -import { VoiceCallConfigSchema } from "./config.js"; - -// Legacy voice-call config warnings and doctor-fix migration helpers. - -/** Version where legacy voice-call config shape support is removed. */ -export const VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION = "2026.6.0"; - -/** One legacy config issue with the replacement path and message. */ -type VoiceCallLegacyConfigIssue = { - path: string; - replacement: string; - message: string; -}; const asObject = asOptionalRecord; const getString = readStringField; @@ -45,95 +31,6 @@ function mergeProviderConfig( }; } -/** Collect legacy voice-call config keys that should be migrated. */ -export function collectVoiceCallLegacyConfigIssues(value: unknown): VoiceCallLegacyConfigIssue[] { - const raw = asObject(value) ?? {}; - const realtime = asObject(raw.realtime); - const realtimeAgentContext = asObject(realtime?.agentContext); - const twilio = asObject(raw.twilio); - const streaming = asObject(raw.streaming); - - const issues: VoiceCallLegacyConfigIssue[] = []; - if (raw.provider === "log") { - issues.push({ - path: "provider", - replacement: "provider", - message: 'Replace provider "log" with "mock".', - }); - } - if (typeof twilio?.from === "string") { - issues.push({ - path: "twilio.from", - replacement: "fromNumber", - message: "Move twilio.from to fromNumber.", - }); - } - if (typeof streaming?.sttProvider === "string") { - issues.push({ - path: "streaming.sttProvider", - replacement: "streaming.provider", - message: "Move streaming.sttProvider to streaming.provider.", - }); - } - if (typeof streaming?.openaiApiKey === "string") { - issues.push({ - path: "streaming.openaiApiKey", - replacement: "streaming.providers.openai.apiKey", - message: "Move streaming.openaiApiKey to streaming.providers.openai.apiKey.", - }); - } - if (typeof streaming?.sttModel === "string") { - issues.push({ - path: "streaming.sttModel", - replacement: "streaming.providers.openai.model", - message: "Move streaming.sttModel to streaming.providers.openai.model.", - }); - } - if (typeof streaming?.silenceDurationMs === "number") { - issues.push({ - path: "streaming.silenceDurationMs", - replacement: "streaming.providers.openai.silenceDurationMs", - message: "Move streaming.silenceDurationMs to streaming.providers.openai.silenceDurationMs.", - }); - } - if (typeof streaming?.vadThreshold === "number") { - issues.push({ - path: "streaming.vadThreshold", - replacement: "streaming.providers.openai.vadThreshold", - message: "Move streaming.vadThreshold to streaming.providers.openai.vadThreshold.", - }); - } - if (realtimeAgentContext && Object.hasOwn(realtimeAgentContext, "includeSystemPrompt")) { - issues.push({ - path: "realtime.agentContext.includeSystemPrompt", - replacement: "realtime.agentContext", - message: - "Remove realtime.agentContext.includeSystemPrompt; realtime context now uses the generated agent prompt.", - }); - } - - return issues; -} - -/** Format runtime warnings for legacy voice-call config keys. */ -export function formatVoiceCallLegacyConfigWarnings(params: { - value: unknown; - configPathPrefix: string; - doctorFixCommand: string; -}): string[] { - const issues = collectVoiceCallLegacyConfigIssues(params.value); - if (issues.length === 0) { - return []; - } - - return [ - `[voice-call] legacy config keys detected under ${params.configPathPrefix}; runtime loading will not rewrite them, and support for the legacy shape will be removed in ${VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION}. Run "${params.doctorFixCommand}".`, - ...issues.map( - (issue) => `[voice-call] ${params.configPathPrefix}.${issue.path}: ${issue.message}`, - ), - ]; -} - /** Migrate legacy voice-call config input to the current canonical shape. */ export function migrateVoiceCallLegacyConfigInput(params: { value: unknown; @@ -141,7 +38,6 @@ export function migrateVoiceCallLegacyConfigInput(params: { }): { config: Record; changes: string[]; - issues: VoiceCallLegacyConfigIssue[]; } { const raw = asObject(params.value) ?? {}; const realtime = asObject(raw.realtime); @@ -149,7 +45,6 @@ export function migrateVoiceCallLegacyConfigInput(params: { const twilio = asObject(raw.twilio); const streaming = asObject(raw.streaming); const configPathPrefix = params.configPathPrefix ?? "plugins.entries.voice-call.config"; - const issues = collectVoiceCallLegacyConfigIssues(raw); const legacyStreamingOpenAICompat: Record = {}; const streamingOpenAIApiKey = getString(streaming, "openaiApiKey"); @@ -261,15 +156,5 @@ export function migrateVoiceCallLegacyConfigInput(params: { changes.push(`Removed ${configPathPrefix}.realtime.agentContext.includeSystemPrompt.`); } - return { config, changes, issues }; -} - -/** Normalize legacy voice-call config input without returning migration metadata. */ -export function normalizeVoiceCallLegacyConfigInput(value: unknown): Record { - return migrateVoiceCallLegacyConfigInput({ value }).config; -} - -/** Parse voice-call plugin config after applying legacy normalization. */ -export function parseVoiceCallPluginConfig(value: unknown): VoiceCallConfig { - return VoiceCallConfigSchema.parse(normalizeVoiceCallLegacyConfigInput(value)); + return { config, changes }; }