From fa0e298d8491fd0e86d0cff918a42e91e5485aa9 Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:06:19 -0700 Subject: [PATCH] fix(ios): route forced realtime consults through Gateway relay (#117485) * fix(ios): route forced realtime consults through Gateway relay * fix(ios): route forced realtime consults through Gateway relay * fix(ios): route forced realtime consults through Gateway relay * fix(ios): route forced realtime consults through Gateway relay * style(ios): drop redundant Self in requiresForcedAgentConsultRelay * style(ios): use explicit self for firstString per swiftformat --------- Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: Peter Steinberger --- .../Sources/Voice/TalkModeGatewayConfig.swift | 13 +++- .../Tests/TalkModeConfigParsingTests.swift | 70 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) 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`() {