diff --git a/src/gateway/protocol/index.test.ts b/src/gateway/protocol/index.test.ts index 3554f326278..ad452effd1f 100644 --- a/src/gateway/protocol/index.test.ts +++ b/src/gateway/protocol/index.test.ts @@ -99,4 +99,21 @@ describe("validateTalkConfigResult", () => { }), ).toBe(true); }); + + it("rejects normalized talk payloads without talk.resolved", () => { + expect( + validateTalkConfigResult({ + config: { + talk: { + provider: "elevenlabs", + providers: { + elevenlabs: { + voiceId: "voice-normalized", + }, + }, + }, + }, + }), + ).toBe(false); + }); }); diff --git a/src/gateway/protocol/schema/channels.ts b/src/gateway/protocol/schema/channels.ts index f53c1f5302b..ee4d6d1ea1f 100644 --- a/src/gateway/protocol/schema/channels.ts +++ b/src/gateway/protocol/schema/channels.ts @@ -35,27 +35,40 @@ const ResolvedTalkConfigSchema = Type.Object( { additionalProperties: false }, ); +const LegacyTalkConfigSchema = Type.Object( + { + voiceId: Type.Optional(Type.String()), + voiceAliases: Type.Optional(Type.Record(Type.String(), Type.String())), + modelId: Type.Optional(Type.String()), + outputFormat: Type.Optional(Type.String()), + apiKey: Type.Optional(SecretInputSchema), + interruptOnSpeech: Type.Optional(Type.Boolean()), + silenceTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })), + }, + { additionalProperties: false }, +); + +const NormalizedTalkConfigSchema = Type.Object( + { + provider: Type.Optional(Type.String()), + providers: Type.Optional(Type.Record(Type.String(), TalkProviderConfigSchema)), + resolved: ResolvedTalkConfigSchema, + voiceId: Type.Optional(Type.String()), + voiceAliases: Type.Optional(Type.Record(Type.String(), Type.String())), + modelId: Type.Optional(Type.String()), + outputFormat: Type.Optional(Type.String()), + apiKey: Type.Optional(SecretInputSchema), + interruptOnSpeech: Type.Optional(Type.Boolean()), + silenceTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })), + }, + { additionalProperties: false }, +); + export const TalkConfigResultSchema = Type.Object( { config: Type.Object( { - talk: Type.Optional( - Type.Object( - { - provider: Type.Optional(Type.String()), - providers: Type.Optional(Type.Record(Type.String(), TalkProviderConfigSchema)), - resolved: Type.Optional(ResolvedTalkConfigSchema), - voiceId: Type.Optional(Type.String()), - voiceAliases: Type.Optional(Type.Record(Type.String(), Type.String())), - modelId: Type.Optional(Type.String()), - outputFormat: Type.Optional(Type.String()), - apiKey: Type.Optional(SecretInputSchema), - interruptOnSpeech: Type.Optional(Type.Boolean()), - silenceTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })), - }, - { additionalProperties: false }, - ), - ), + talk: Type.Optional(Type.Union([LegacyTalkConfigSchema, NormalizedTalkConfigSchema])), session: Type.Optional( Type.Object( { diff --git a/src/gateway/protocol/talk-config.contract.test.ts b/src/gateway/protocol/talk-config.contract.test.ts index 59cac413a90..7d77dbc37ca 100644 --- a/src/gateway/protocol/talk-config.contract.test.ts +++ b/src/gateway/protocol/talk-config.contract.test.ts @@ -30,7 +30,7 @@ describe("talk.config contract fixtures", () => { if (fixture.payloadValid) { expect(validateTalkConfigResult(payload)).toBe(true); } else { - expect((payload.config.talk as { resolved?: unknown }).resolved).toBeUndefined(); + expect(validateTalkConfigResult(payload)).toBe(false); } if (!fixture.expectedSelection) {