From 635e9cc7202c0ecc35397b440c49d9b55d7c2691 Mon Sep 17 00:00:00 2001 From: Panha Sim Date: Tue, 3 Mar 2026 10:10:46 -0700 Subject: [PATCH 1/2] Handle Realtime MCP event response.mcp_call.completed --- .../Private/Realtime/OpenAIRealtimeSession.swift | 11 ++++++++++- .../Realtime/OpenAIRealtimeMessage.swift | 7 +++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAI/Private/Realtime/OpenAIRealtimeSession.swift b/Sources/OpenAI/Private/Realtime/OpenAIRealtimeSession.swift index cdd1ab0..afc5b0f 100644 --- a/Sources/OpenAI/Private/Realtime/OpenAIRealtimeSession.swift +++ b/Sources/OpenAI/Private/Realtime/OpenAIRealtimeSession.swift @@ -248,7 +248,16 @@ open class OpenAIRealtimeSession { "Top-level fields: message=\(String(describing: topLevelMessage)), code=\(String(describing: topLevelCode)), reason=\(String(describing: topLevelReason))") continuation?.yield(.mcpListToolsFailed(fullError)) - + + case "response.mcp_call.completed": + let eventId = json["event_id"] as? String + let itemId = json["item_id"] as? String + let outputIndex = json["output_index"] as? Int + continuation?.yield(.responseMcpCallCompleted(eventId: eventId, itemId: itemId, outputIndex: outputIndex)) + + case "response.mcp_call.in_progress": + continuation?.yield(.responseMcpCallInProgress) + case "response.done": // Handle response completion (may contain errors like insufficient_quota) if diff --git a/Sources/OpenAI/Public/ResponseModels/Realtime/OpenAIRealtimeMessage.swift b/Sources/OpenAI/Public/ResponseModels/Realtime/OpenAIRealtimeMessage.swift index bb551a9..c563028 100644 --- a/Sources/OpenAI/Public/ResponseModels/Realtime/OpenAIRealtimeMessage.swift +++ b/Sources/OpenAI/Public/ResponseModels/Realtime/OpenAIRealtimeMessage.swift @@ -26,7 +26,6 @@ public enum OpenAIRealtimeMessage: Sendable { case mcpListToolsInProgress // "mcp_list_tools.in_progress" case mcpListToolsCompleted([String: Any]) // "mcp_list_tools.completed" with tools data case mcpListToolsFailed(String?) // "mcp_list_tools.failed" with error details - /// Response completion with potential errors case responseDone(status: String, statusDetails: [String: Any]?) // "response.done" @@ -41,7 +40,11 @@ public enum OpenAIRealtimeMessage: Sendable { // Content part lifecycle case responseContentPartAdded(type: String) // "response.content_part.added" case responseContentPartDone(type: String, text: String?) // "response.content_part.done" - + + // MCP response + case responseMcpCallCompleted(eventId: String?, itemId: String?, outputIndex: Int?) + case responseMcpCallInProgress + /// Conversation item case conversationItemCreated(itemId: String, type: String, role: String?) // "conversation.item.created" } From 9899e97a7ec6d131d589074bfc5cba616a0e4163 Mon Sep 17 00:00:00 2001 From: Panha Sim Date: Wed, 1 Apr 2026 16:02:01 -0700 Subject: [PATCH 2/2] Apply SwiftFormat Fixes --- .../SwiftOpenAIExample/Files/AttachmentView.swift | 1 + .../ResponseAPIDemo/ResponseStreamDemoView.swift | 1 + .../SwiftOpenAIExample/SharedUI/LoadingView.swift | 5 +++-- .../Private/Audio/MicrophonePCMSampleVendorAT.swift | 10 +++++----- .../Private/Realtime/OpenAIRealtimeSession.swift | 6 +++--- .../Realtime/OpenAIRealtimeMessage.swift | 4 ++-- Sources/OpenAI/Public/Service/OpenAIService.swift | 3 +++ 7 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Examples/SwiftOpenAIExample/SwiftOpenAIExample/Files/AttachmentView.swift b/Examples/SwiftOpenAIExample/SwiftOpenAIExample/Files/AttachmentView.swift index 5d29c7e..c83b9a1 100644 --- a/Examples/SwiftOpenAIExample/SwiftOpenAIExample/Files/AttachmentView.swift +++ b/Examples/SwiftOpenAIExample/SwiftOpenAIExample/Files/AttachmentView.swift @@ -10,6 +10,7 @@ import SwiftUI struct AttachmentView: View { let fileName: String @Binding var actionTrigger: Bool + let isLoading: Bool var body: some View { diff --git a/Examples/SwiftOpenAIExample/SwiftOpenAIExample/ResponseAPIDemo/ResponseStreamDemoView.swift b/Examples/SwiftOpenAIExample/SwiftOpenAIExample/ResponseAPIDemo/ResponseStreamDemoView.swift index 84b561a..195c731 100644 --- a/Examples/SwiftOpenAIExample/SwiftOpenAIExample/ResponseAPIDemo/ResponseStreamDemoView.swift +++ b/Examples/SwiftOpenAIExample/SwiftOpenAIExample/ResponseAPIDemo/ResponseStreamDemoView.swift @@ -145,6 +145,7 @@ struct ResponseStreamDemoView: View { struct MessageBubbleView: View { let message: ResponseStreamProvider.ResponseMessage + @Environment(\.colorScheme) var colorScheme var body: some View { diff --git a/Examples/SwiftOpenAIExample/SwiftOpenAIExample/SharedUI/LoadingView.swift b/Examples/SwiftOpenAIExample/SwiftOpenAIExample/SharedUI/LoadingView.swift index bbceb00..0463b9e 100644 --- a/Examples/SwiftOpenAIExample/SwiftOpenAIExample/SharedUI/LoadingView.swift +++ b/Examples/SwiftOpenAIExample/SwiftOpenAIExample/SharedUI/LoadingView.swift @@ -8,8 +8,6 @@ import SwiftUI struct LoadingView: View { - @State private var dotsCount = 0 - let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect() var body: some View { @@ -28,4 +26,7 @@ struct LoadingView: View { func getDots() -> String { String(repeating: ".", count: dotsCount) } + + @State private var dotsCount = 0 + } diff --git a/Sources/OpenAI/Private/Audio/MicrophonePCMSampleVendorAT.swift b/Sources/OpenAI/Private/Audio/MicrophonePCMSampleVendorAT.swift index b957851..c9e63ef 100644 --- a/Sources/OpenAI/Private/Audio/MicrophonePCMSampleVendorAT.swift +++ b/Sources/OpenAI/Private/Audio/MicrophonePCMSampleVendorAT.swift @@ -278,11 +278,11 @@ class MicrophonePCMSampleVendorAT: MicrophonePCMSampleVendor { /// This @RealtimeActor annotation is a lie. @RealtimeActor private let audioRenderCallback: AURenderCallback = { inRefCon, - ioActionFlags, - inTimeStamp, - inBusNumber, - inNumberFrames, - _ in + ioActionFlags, + inTimeStamp, + inBusNumber, + inNumberFrames, + _ in let microphonePCMSampleVendor = Unmanaged .fromOpaque(inRefCon) .takeUnretainedValue() diff --git a/Sources/OpenAI/Private/Realtime/OpenAIRealtimeSession.swift b/Sources/OpenAI/Private/Realtime/OpenAIRealtimeSession.swift index afc5b0f..0435f6a 100644 --- a/Sources/OpenAI/Private/Realtime/OpenAIRealtimeSession.swift +++ b/Sources/OpenAI/Private/Realtime/OpenAIRealtimeSession.swift @@ -248,16 +248,16 @@ open class OpenAIRealtimeSession { "Top-level fields: message=\(String(describing: topLevelMessage)), code=\(String(describing: topLevelCode)), reason=\(String(describing: topLevelReason))") continuation?.yield(.mcpListToolsFailed(fullError)) - + case "response.mcp_call.completed": let eventId = json["event_id"] as? String let itemId = json["item_id"] as? String let outputIndex = json["output_index"] as? Int continuation?.yield(.responseMcpCallCompleted(eventId: eventId, itemId: itemId, outputIndex: outputIndex)) - + case "response.mcp_call.in_progress": continuation?.yield(.responseMcpCallInProgress) - + case "response.done": // Handle response completion (may contain errors like insufficient_quota) if diff --git a/Sources/OpenAI/Public/ResponseModels/Realtime/OpenAIRealtimeMessage.swift b/Sources/OpenAI/Public/ResponseModels/Realtime/OpenAIRealtimeMessage.swift index c563028..e46f757 100644 --- a/Sources/OpenAI/Public/ResponseModels/Realtime/OpenAIRealtimeMessage.swift +++ b/Sources/OpenAI/Public/ResponseModels/Realtime/OpenAIRealtimeMessage.swift @@ -40,11 +40,11 @@ public enum OpenAIRealtimeMessage: Sendable { // Content part lifecycle case responseContentPartAdded(type: String) // "response.content_part.added" case responseContentPartDone(type: String, text: String?) // "response.content_part.done" - + // MCP response case responseMcpCallCompleted(eventId: String?, itemId: String?, outputIndex: Int?) case responseMcpCallInProgress - + /// Conversation item case conversationItemCreated(itemId: String, type: String, role: String?) // "conversation.item.created" } diff --git a/Sources/OpenAI/Public/Service/OpenAIService.swift b/Sources/OpenAI/Public/Service/OpenAIService.swift index 525bdaf..a0025ae 100644 --- a/Sources/OpenAI/Public/Service/OpenAIService.swift +++ b/Sources/OpenAI/Public/Service/OpenAIService.swift @@ -1419,9 +1419,11 @@ extension OpenAIService { case .threadMessageDelta: let decoded = try self.decoder.decode(MessageDeltaObject.self, from: data) continuation.yield(.threadMessageDelta(decoded)) + case .threadRunStepDelta: let decoded = try self.decoder.decode(RunStepDeltaObject.self, from: data) continuation.yield(.threadRunStepDelta(decoded)) + case .threadRun: // We expect a object of type "thread.run.SOME_STATE" in the data object // However what we get is a `thread.run` object but we can check the status @@ -1454,6 +1456,7 @@ extension OpenAIService { } #endif } + default: #if DEBUG if debugEnabled {