From 88ff303b2c5555c747d462a525b466536ac20d9b Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Thu, 10 Jul 2025 18:30:41 +0400 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Vapi's=20Latest=20Tool?= =?UTF-8?q?-Calling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/avocaddo/vapi-client-sdk-ios/commit/40ab95ee35a685010eb86162924e2055096ddccf https://github.com/evekeen/client-sdk-ios/commit/4c140ebe4a2b8c3a6b4bccdd42a745438ddcf6c6 --- Sources/Models/AppMessage.swift | 1 + Sources/Models/ConversationUpdate.swift | 6 ++++- Sources/Models/ToolCall.swift | 32 +++++++++++++++++++++++++ Sources/Vapi.swift | 27 +++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 Sources/Models/ToolCall.swift diff --git a/Sources/Models/AppMessage.swift b/Sources/Models/AppMessage.swift index 0f9c545..04dffb2 100644 --- a/Sources/Models/AppMessage.swift +++ b/Sources/Models/AppMessage.swift @@ -11,6 +11,7 @@ struct AppMessage: Codable { enum MessageType: String, Codable { case hang case functionCall = "function-call" + case toolCalls = "tool-calls" case transcript case speechUpdate = "speech-update" case metadata diff --git a/Sources/Models/ConversationUpdate.swift b/Sources/Models/ConversationUpdate.swift index 917a3e8..cd42ac7 100644 --- a/Sources/Models/ConversationUpdate.swift +++ b/Sources/Models/ConversationUpdate.swift @@ -5,10 +5,14 @@ public struct Message: Codable { case user = "user" case assistant = "assistant" case system = "system" + case tool = "tool" } public let role: Role - public let content: String + public let content: String? // For role=tool, has the response of the tool call. For role=assistant with tool_calls is nil. + public let tool_calls: [ToolCall]? // Only for role=assistant with tool calls. + public let tool_call_id: String? // Only for role=tool, with tool response. + } public struct ConversationUpdate: Codable { diff --git a/Sources/Models/ToolCall.swift b/Sources/Models/ToolCall.swift new file mode 100644 index 0000000..79f72a7 --- /dev/null +++ b/Sources/Models/ToolCall.swift @@ -0,0 +1,32 @@ +// +// ToolCall.swift +// +// +// Created by Anton Begehr on 2025-07-10. +// + +import Foundation + +public struct ToolCall: Codable { + enum CodingKeys: CodingKey { + case id + case type + case function + } + + public let id: String + public let type: String + public let function: Function +} + +public extension ToolCall { + struct Function: Codable { + enum CodingKeys: CodingKey { + case name + case arguments + } + + public let name: String + public let arguments: String // TODO: actually is [String: Any] + } +} diff --git a/Sources/Vapi.swift b/Sources/Vapi.swift index 69bdaa3..bac2e1d 100644 --- a/Sources/Vapi.swift +++ b/Sources/Vapi.swift @@ -42,6 +42,7 @@ public final class Vapi: CallClientDelegate { case callDidEnd case transcript(Transcript) case functionCall(FunctionCall) + case toolCalls([ToolCall]) case speechUpdate(SpeechUpdate) case metadata(Metadata) case conversationUpdate(ConversationUpdate) @@ -465,6 +466,32 @@ public final class Vapi: CallClientDelegate { let functionCall = FunctionCall(name: name, parameters: parameters) event = Event.functionCall(functionCall) + case .toolCalls: + guard let messageDictionary = try JSONSerialization.jsonObject(with: unescapedData, options: []) as? [String: Any] else { + throw VapiError.decodingError(message: "App message isn't a valid JSON object") + } + guard let toolsCallsArray = messageDictionary["toolCalls"] as? [[String: Any]] else { + throw VapiError.decodingError(message: "App message missing toolCalls") + } + let toolCalls: [ToolCall] = try toolsCallsArray.map { dict in + guard let id = dict[ToolCall.CodingKeys.id.stringValue] as? String else { + throw VapiError.decodingError(message: "ToolCall missing id") + } + guard let type = dict[ToolCall.CodingKeys.id.stringValue] as? String else { + throw VapiError.decodingError(message: "ToolCall missing type") + } + guard let functionDict = dict[ToolCall.CodingKeys.id.stringValue] as? [String: Any] else { + throw VapiError.decodingError(message: "ToolCall missing function") + } + guard let name = functionDict[ToolCall.Function.CodingKeys.name.stringValue] as? String else { + throw VapiError.decodingError(message: "ToolCall missing function name") + } + guard let arguments = functionDict[ToolCall.Function.CodingKeys.arguments.stringValue] as? String else { + throw VapiError.decodingError(message: "ToolCall missing function arguments") + } + return .init(id: id, type: type, function: .init(name: name, arguments: arguments)) + } + event = Event.toolCalls(toolCalls) case .hang: event = Event.hang case .transcript: From 3de0c2bb67b25082bb33b1dfcc7751eca941d811 Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Thu, 10 Jul 2025 18:40:10 +0400 Subject: [PATCH 2/8] error descriptions --- Sources/Models/VapiError.swift | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/Models/VapiError.swift b/Sources/Models/VapiError.swift index 9502956..5c9910f 100644 --- a/Sources/Models/VapiError.swift +++ b/Sources/Models/VapiError.swift @@ -7,7 +7,7 @@ import Foundation -public enum VapiError: Swift.Error { +public enum VapiError: LocalizedError { case invalidURL case customError(String) case existingCallInProgress @@ -15,3 +15,22 @@ public enum VapiError: Swift.Error { case decodingError(message: String, response: String? = nil) case invalidJsonData } + +extension VapiError { + public var errorDescription: String? { + switch self { + case .invalidURL: + return "URL is invalid" + case .customError(let message): + return message + case .existingCallInProgress: + return "An existing call is in progress" + case .noCallInProgress: + return "No call in progress" + case .decodingError(let message, let response): + return "\(message)\n\(response ?? "No response data")" + case .invalidJsonData: + return "Invalid JSON data" + } + } +} From 4c8fe88b43cca7e52ef73fa235c36b9e444251db Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Thu, 10 Jul 2025 18:43:56 +0400 Subject: [PATCH 3/8] upd --- Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.resolved b/Package.resolved index c06eb6f..63c9dd5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/daily-co/daily-client-ios", "state" : { - "revision" : "1b84803a17766240007f11c553ca7debbfcef33b", - "version" : "0.22.0" + "revision" : "431938db25e5807120e89e2dc5bab1c076729f59", + "version" : "0.31.0" } } ], From 04bde59828ffee59e1b2b11da6491cb55d53cc56 Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Thu, 10 Jul 2025 18:47:25 +0400 Subject: [PATCH 4/8] fix parsing tool call --- Sources/Models/VapiError.swift | 2 +- Sources/Vapi.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Models/VapiError.swift b/Sources/Models/VapiError.swift index 5c9910f..f4414b4 100644 --- a/Sources/Models/VapiError.swift +++ b/Sources/Models/VapiError.swift @@ -28,7 +28,7 @@ extension VapiError { case .noCallInProgress: return "No call in progress" case .decodingError(let message, let response): - return "\(message)\n\(response ?? "No response data")" + return "\(message), \(response ?? "no response")" case .invalidJsonData: return "Invalid JSON data" } diff --git a/Sources/Vapi.swift b/Sources/Vapi.swift index bac2e1d..2c4f9ce 100644 --- a/Sources/Vapi.swift +++ b/Sources/Vapi.swift @@ -477,10 +477,10 @@ public final class Vapi: CallClientDelegate { guard let id = dict[ToolCall.CodingKeys.id.stringValue] as? String else { throw VapiError.decodingError(message: "ToolCall missing id") } - guard let type = dict[ToolCall.CodingKeys.id.stringValue] as? String else { + guard let type = dict[ToolCall.CodingKeys.type.stringValue] as? String else { throw VapiError.decodingError(message: "ToolCall missing type") } - guard let functionDict = dict[ToolCall.CodingKeys.id.stringValue] as? [String: Any] else { + guard let functionDict = dict[ToolCall.CodingKeys.function.stringValue] as? [String: Any] else { throw VapiError.decodingError(message: "ToolCall missing function") } guard let name = functionDict[ToolCall.Function.CodingKeys.name.stringValue] as? String else { From d338e0f2cc9549dca6ff25cafeb07abce39c06fe Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Thu, 10 Jul 2025 18:55:56 +0400 Subject: [PATCH 5/8] use AnyCodable --- Sources/Models/AnyCodable.swift | 80 +++++++++++++++++++++++++++++++++ Sources/Models/ToolCall.swift | 2 +- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 Sources/Models/AnyCodable.swift diff --git a/Sources/Models/AnyCodable.swift b/Sources/Models/AnyCodable.swift new file mode 100644 index 0000000..6cf0b15 --- /dev/null +++ b/Sources/Models/AnyCodable.swift @@ -0,0 +1,80 @@ +// +// AnyCodable.swift +// +// +// Created by Anton Begehr on 2025-07-10. +// + +import Foundation + +public enum AnyCodable: Codable { + case string(String) + case int(Int) + case double(Double) + case bool(Bool) + case array([AnyCodable]) + case dictionary([String: AnyCodable]) + case null + + public init(_ value: Any?) { + switch value { + case let string as String: + self = .string(string) + case let int as Int: + self = .int(int) + case let double as Double: + self = .double(double) + case let bool as Bool: + self = .bool(bool) + case let array as [Any]: + self = .array(array.map { AnyCodable($0) }) + case let dict as [String: Any]: + self = .dictionary(dict.mapValues { AnyCodable($0) }) + case nil: + self = .null + default: + self = .null // Fallback for unsupported types + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let string = try? container.decode(String.self) { + self = .string(string) + } else if let int = try? container.decode(Int.self) { + self = .int(int) + } else if let double = try? container.decode(Double.self) { + self = .double(double) + } else if let bool = try? container.decode(Bool.self) { + self = .bool(bool) + } else if let array = try? container.decode([AnyCodable].self) { + self = .array(array) + } else if let dict = try? container.decode([String: AnyCodable].self) { + self = .dictionary(dict) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON type") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): + try container.encode(value) + case .int(let value): + try container.encode(value) + case .double(let value): + try container.encode(value) + case .bool(let value): + try container.encode(value) + case .array(let value): + try container.encode(value) + case .dictionary(let value): + try container.encode(value) + case .null: + try container.encodeNil() + } + } +} diff --git a/Sources/Models/ToolCall.swift b/Sources/Models/ToolCall.swift index 79f72a7..da38d75 100644 --- a/Sources/Models/ToolCall.swift +++ b/Sources/Models/ToolCall.swift @@ -27,6 +27,6 @@ public extension ToolCall { } public let name: String - public let arguments: String // TODO: actually is [String: Any] + public let arguments: [String: AnyCodable] } } From 4c837754a806714c0e21ee7665d1b4c6c6671492 Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Thu, 10 Jul 2025 18:59:50 +0400 Subject: [PATCH 6/8] decode toolcalls --- .../{ToolCall.swift => ToolCalls.swift} | 6 ++++- Sources/Vapi.swift | 27 ++----------------- 2 files changed, 7 insertions(+), 26 deletions(-) rename Sources/Models/{ToolCall.swift => ToolCalls.swift} (85%) diff --git a/Sources/Models/ToolCall.swift b/Sources/Models/ToolCalls.swift similarity index 85% rename from Sources/Models/ToolCall.swift rename to Sources/Models/ToolCalls.swift index da38d75..b6cc387 100644 --- a/Sources/Models/ToolCall.swift +++ b/Sources/Models/ToolCalls.swift @@ -1,5 +1,5 @@ // -// ToolCall.swift +// ToolCalls.swift // // // Created by Anton Begehr on 2025-07-10. @@ -7,6 +7,10 @@ import Foundation +public struct ToolCalls: Codable { + public let toolCalls: [ToolCall] +} + public struct ToolCall: Codable { enum CodingKeys: CodingKey { case id diff --git a/Sources/Vapi.swift b/Sources/Vapi.swift index 2c4f9ce..11fe88f 100644 --- a/Sources/Vapi.swift +++ b/Sources/Vapi.swift @@ -467,31 +467,8 @@ public final class Vapi: CallClientDelegate { let functionCall = FunctionCall(name: name, parameters: parameters) event = Event.functionCall(functionCall) case .toolCalls: - guard let messageDictionary = try JSONSerialization.jsonObject(with: unescapedData, options: []) as? [String: Any] else { - throw VapiError.decodingError(message: "App message isn't a valid JSON object") - } - guard let toolsCallsArray = messageDictionary["toolCalls"] as? [[String: Any]] else { - throw VapiError.decodingError(message: "App message missing toolCalls") - } - let toolCalls: [ToolCall] = try toolsCallsArray.map { dict in - guard let id = dict[ToolCall.CodingKeys.id.stringValue] as? String else { - throw VapiError.decodingError(message: "ToolCall missing id") - } - guard let type = dict[ToolCall.CodingKeys.type.stringValue] as? String else { - throw VapiError.decodingError(message: "ToolCall missing type") - } - guard let functionDict = dict[ToolCall.CodingKeys.function.stringValue] as? [String: Any] else { - throw VapiError.decodingError(message: "ToolCall missing function") - } - guard let name = functionDict[ToolCall.Function.CodingKeys.name.stringValue] as? String else { - throw VapiError.decodingError(message: "ToolCall missing function name") - } - guard let arguments = functionDict[ToolCall.Function.CodingKeys.arguments.stringValue] as? String else { - throw VapiError.decodingError(message: "ToolCall missing function arguments") - } - return .init(id: id, type: type, function: .init(name: name, arguments: arguments)) - } - event = Event.toolCalls(toolCalls) + let toolCalls = try decoder.decode(ToolCalls.self, from: unescapedData) + event = Event.toolCalls(toolCalls.toolCalls) case .hang: event = Event.hang case .transcript: From c871846a51a516f8b3f320bab1a76110621b38b1 Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Thu, 10 Jul 2025 19:11:50 +0400 Subject: [PATCH 7/8] correctly parse tool_calls on convo messages [String: AnyCodable] --- Sources/Models/ConversationUpdate.swift | 13 ++++++++++--- Sources/Models/ToolCalls.swift | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/Models/ConversationUpdate.swift b/Sources/Models/ConversationUpdate.swift index cd42ac7..7c95dc8 100644 --- a/Sources/Models/ConversationUpdate.swift +++ b/Sources/Models/ConversationUpdate.swift @@ -7,11 +7,18 @@ public struct Message: Codable { case system = "system" case tool = "tool" } - + + enum CodingKeys: String, CodingKey { + case role + case content + case toolCalls = "tool_calls" + case toolCallId = "tool_call_id" + } + public let role: Role public let content: String? // For role=tool, has the response of the tool call. For role=assistant with tool_calls is nil. - public let tool_calls: [ToolCall]? // Only for role=assistant with tool calls. - public let tool_call_id: String? // Only for role=tool, with tool response. + public let toolCalls: [ToolCall]? // Only for role=assistant with tool calls. + public let toolCallId: String? // Only for role=tool, with tool response. } diff --git a/Sources/Models/ToolCalls.swift b/Sources/Models/ToolCalls.swift index b6cc387..c3bd1c2 100644 --- a/Sources/Models/ToolCalls.swift +++ b/Sources/Models/ToolCalls.swift @@ -31,6 +31,6 @@ public extension ToolCall { } public let name: String - public let arguments: [String: AnyCodable] + public let arguments: AnyCodable // In `conversation-update.messages`, this will be an encoded string. In `tool-calls.toolCalls`, this will be a dictionary. } } From ce072ed6c51592bc3477038c0605b4980db42ab5 Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Thu, 10 Jul 2025 19:21:19 +0400 Subject: [PATCH 8/8] clean --- Sources/Models/ToolCalls.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Models/ToolCalls.swift b/Sources/Models/ToolCalls.swift index c3bd1c2..778d565 100644 --- a/Sources/Models/ToolCalls.swift +++ b/Sources/Models/ToolCalls.swift @@ -31,6 +31,6 @@ public extension ToolCall { } public let name: String - public let arguments: AnyCodable // In `conversation-update.messages`, this will be an encoded string. In `tool-calls.toolCalls`, this will be a dictionary. + public let arguments: AnyCodable // In `conversation-update`, this will be an encoded string. In `tool-calls`, this will be a dictionary. } }