diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index 75b48a0e61c4..7b91ecc901a7 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -2916,6 +2916,19 @@ describe("doctor config flow", () => { modelId: "eleven_v3", }, }, + realtime: { + provider: "openai", + providers: { + openai: { + model: "gpt-realtime", + }, + }, + model: "gpt-realtime", + voice: "cedar", + mode: "realtime", + transport: "gateway-relay", + brain: "agent-consult", + }, }, }, null, diff --git a/src/commands/doctor-legacy-config.migrations.test.ts b/src/commands/doctor-legacy-config.migrations.test.ts index ee95de6e7cd2..9deee0bafb6d 100644 --- a/src/commands/doctor-legacy-config.migrations.test.ts +++ b/src/commands/doctor-legacy-config.migrations.test.ts @@ -1251,6 +1251,37 @@ describe("normalizeCompatibilityConfigValues", () => { expect(res.changes).toStrictEqual([]); }); + it("does not report talk provider normalization for realtime voice aliases", () => { + const input = { + talk: { + provider: "elevenlabs", + providers: { + elevenlabs: { + voiceId: "voice-123", + }, + }, + realtime: { + provider: "openai", + providers: { + openai: { + model: "gpt-realtime", + }, + }, + model: "gpt-realtime", + voice: "cedar", + mode: "realtime", + transport: "gateway-relay", + brain: "agent-consult", + }, + }, + }; + + const res = normalizeCompatibilityConfigValues(input as OpenClawConfig); + + expect(res.config).toEqual(input); + expect(res.changes).toStrictEqual([]); + }); + it("migrates tools.message.allowCrossContextSend to canonical crossContext settings", () => { const res = normalizeCompatibilityConfigValues({ tools: { diff --git a/src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts b/src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts index 7923329a216b..1b9cdc28cd85 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts @@ -102,6 +102,38 @@ describe("legacy migrate provider-shaped config", () => { }); }); + it("does not treat an existing realtime voice alias as Talk provider repair", () => { + const input: OpenClawConfig = { + talk: { + provider: "elevenlabs", + providers: { + elevenlabs: { + voiceId: "voice-1", + }, + }, + realtime: { + provider: "openai", + providers: { + openai: { + model: "gpt-realtime", + }, + }, + model: "gpt-realtime", + voice: "cedar", + mode: "realtime", + transport: "gateway-relay", + brain: "agent-consult", + }, + }, + }; + const changes: string[] = []; + + const migrated = normalizeLegacyTalkConfig(input, changes); + + expect(changes).toStrictEqual([]); + expect(migrated).toEqual(input); + }); + it("moves messages.tts. keys into messages.tts.providers", () => { const res = migrateLegacyConfig({ messages: { diff --git a/src/commands/doctor/shared/legacy-talk-config-normalizer.ts b/src/commands/doctor/shared/legacy-talk-config-normalizer.ts index edcc8d4bf513..0ade6ff8cf13 100644 --- a/src/commands/doctor/shared/legacy-talk-config-normalizer.ts +++ b/src/commands/doctor/shared/legacy-talk-config-normalizer.ts @@ -41,6 +41,27 @@ function buildLegacyRealtimeTalkCompat( return normalizeTalkSection({ realtime: compat } as OpenClawConfig["talk"])?.realtime; } +function removeDerivedRealtimeSpeakerVoice( + rawTalk: Record, + normalizedTalk: NonNullable, +): void { + const rawRealtime = rawTalk.realtime; + const normalizedRealtime = normalizedTalk.realtime; + if ( + !isRecord(rawRealtime) || + !normalizedRealtime || + rawRealtime.speakerVoice !== undefined || + normalizedRealtime.speakerVoice === undefined || + normalizedRealtime.speakerVoice !== normalizedRealtime.voice + ) { + return; + } + + // Runtime clients still get speakerVoice from the deprecated voice alias, but + // doctor should not persist that derived value or report it as provider repair. + delete normalizedRealtime.speakerVoice; +} + /** Normalize legacy Talk provider/realtime fields into current talk.providers and talk.realtime. */ export function normalizeLegacyTalkConfig(cfg: OpenClawConfig, changes: string[]): OpenClawConfig { const rawTalk = cfg.talk; @@ -66,6 +87,7 @@ export function normalizeLegacyTalkConfig(cfg: OpenClawConfig, changes: string[] ...normalizedTalk.realtime, }; } + removeDerivedRealtimeSpeakerVoice(rawTalk, normalizedTalk); if (Object.keys(normalizedTalk).length === 0 || isDeepStrictEqual(normalizedTalk, rawTalk)) { return cfg; }