diff --git a/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift b/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift index 03d0e918c114..ab0eedb25b6d 100644 --- a/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift +++ b/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift @@ -401,13 +401,18 @@ enum TalkModeGatewayConfigParser { let realtimeVoiceId = Self.firstString(realtime, keys: ["voice"]) ?? Self.firstString(realtimeProviderConfig, keys: ["voice"]) let realtimeTransport = Self.firstString(realtime, keys: ["transport"])?.lowercased() - let requiresGatewayRealtimeTransport = realtimeTransport == "gateway-relay" + // Direct provider WebRTC can answer before consulting the agent, so this explicit + // policy must stay on the relay that enforces final-transcript consultations. + let requiresForcedAgentConsultRelay = Self.requiresForcedAgentConsultRelay(realtime) + let requiresGatewayRealtimeTransport = requiresForcedAgentConsultRelay + || realtimeTransport == "gateway-relay" || realtimeTransport == "provider-websocket" || Self.usesAzureOpenAI(provider: realtimeProvider, config: realtimeProviderConfig) let openAIProviderConfig = Self.realtimeProviderConfig( providers: realtimeProviders, provider: "openai") - let openAIRequiresGatewayRealtimeTransport = realtimeTransport == "gateway-relay" + let openAIRequiresGatewayRealtimeTransport = requiresForcedAgentConsultRelay + || realtimeTransport == "gateway-relay" || realtimeTransport == "provider-websocket" || Self.usesAzureOpenAI(provider: "openai", config: openAIProviderConfig) let executionMode = Self.resolvedExecutionMode( @@ -450,6 +455,10 @@ enum TalkModeGatewayConfigParser { return nil } + private static func requiresForcedAgentConsultRelay(_ realtime: [String: AnyCodable]?) -> Bool { + self.firstString(realtime, keys: ["consultRouting"])?.lowercased() == "force-agent-consult" + } + private static func resolvedExecutionMode( _ realtime: [String: AnyCodable]?, requiresGatewayRealtimeTransport: Bool) -> TalkModeExecutionMode diff --git a/apps/ios/Tests/TalkModeConfigParsingTests.swift b/apps/ios/Tests/TalkModeConfigParsingTests.swift index 5f463f6d4268..97252991c167 100644 --- a/apps/ios/Tests/TalkModeConfigParsingTests.swift +++ b/apps/ios/Tests/TalkModeConfigParsingTests.swift @@ -635,8 +635,78 @@ struct TalkModeManagerTests { defaultModelIdFallback: "eleven_v3", defaultRealtimeModelIdFallback: "gpt-realtime-2", defaultSilenceTimeoutMs: 900) + let routing = TalkModeRoutingResolver.resolve( + parsed: parsed, + providerSelection: .openAIRealtime, + defaultProvider: "elevenlabs", + defaultRealtimeModelId: "gpt-realtime-2") #expect(parsed.executionMode == .realtimeWebRTC) + #expect(routing.route == .realtimeWebRTC) + } + + @Test func `routes forced agent consultation through gateway relay`() { + let config: [String: Any] = [ + "talk": [ + "realtime": [ + "provider": "openai", + "mode": "realtime", + "brain": "agent-consult", + "consultRouting": "force-agent-consult", + ], + ], + ] + + let parsed = TalkModeGatewayConfigParser.parse( + config: config, + defaultProvider: "elevenlabs", + defaultModelIdFallback: "eleven_v3", + defaultRealtimeModelIdFallback: "gpt-realtime-2", + defaultSilenceTimeoutMs: 900) + let gatewayDefaultRouting = TalkModeRoutingResolver.resolve( + parsed: parsed, + providerSelection: .gatewayDefault, + defaultProvider: "elevenlabs", + defaultRealtimeModelId: "gpt-realtime-2") + let openAIRouting = TalkModeRoutingResolver.resolve( + parsed: parsed, + providerSelection: .openAIRealtime, + defaultProvider: "elevenlabs", + defaultRealtimeModelId: "gpt-realtime-2") + + #expect(parsed.requiresGatewayRealtimeTransport) + #expect(parsed.openAIRequiresGatewayRealtimeTransport) + #expect(parsed.executionMode == .realtimeRelay) + #expect(gatewayDefaultRouting.route == .realtimeRelay) + #expect(openAIRouting.route == .realtimeRelay) + } + + @Test func `routes forced agent consultation through gateway relay without mode or brain`() { + let config: [String: Any] = [ + "talk": [ + "realtime": [ + "provider": "openai", + "consultRouting": "force-agent-consult", + ], + ], + ] + + let parsed = TalkModeGatewayConfigParser.parse( + config: config, + defaultProvider: "elevenlabs", + defaultModelIdFallback: "eleven_v3", + defaultRealtimeModelIdFallback: "gpt-realtime-2", + defaultSilenceTimeoutMs: 900) + let routing = TalkModeRoutingResolver.resolve( + parsed: parsed, + providerSelection: .openAIRealtime, + defaultProvider: "elevenlabs", + defaultRealtimeModelId: "gpt-realtime-2") + + #expect(parsed.requiresGatewayRealtimeTransport) + #expect(parsed.openAIRequiresGatewayRealtimeTransport) + #expect(routing.executionMode == .realtimeRelay) + #expect(routing.route == .realtimeRelay) } @Test func `keeps Azure open AI realtime on gateway relay`() {