Skip to content

🐛 Fix Vapi's Latest Tool-Calling #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 80 additions & 0 deletions Sources/Models/AnyCodable.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
1 change: 1 addition & 0 deletions Sources/Models/AppMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions Sources/Models/ConversationUpdate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@ public struct Message: Codable {
case user = "user"
case assistant = "assistant"
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
public let content: String? // For role=tool, has the response of the tool call. For role=assistant with tool_calls is nil.
public let toolCalls: [ToolCall]? // Only for role=assistant with tool calls.
public let toolCallId: String? // Only for role=tool, with tool response.

}

public struct ConversationUpdate: Codable {
Expand Down
36 changes: 36 additions & 0 deletions Sources/Models/ToolCalls.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// ToolCalls.swift
//
//
// Created by Anton Begehr on 2025-07-10.
//

import Foundation

public struct ToolCalls: Codable {
public let toolCalls: [ToolCall]
}

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: AnyCodable // In `conversation-update`, this will be an encoded string. In `tool-calls`, this will be a dictionary.
}
}
21 changes: 20 additions & 1 deletion Sources/Models/VapiError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,30 @@

import Foundation

public enum VapiError: Swift.Error {
public enum VapiError: LocalizedError {
case invalidURL
case customError(String)
case existingCallInProgress
case noCallInProgress
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), \(response ?? "no response")"
case .invalidJsonData:
return "Invalid JSON data"
}
}
}
4 changes: 4 additions & 0 deletions Sources/Vapi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -465,6 +466,9 @@ public final class Vapi: CallClientDelegate {

let functionCall = FunctionCall(name: name, parameters: parameters)
event = Event.functionCall(functionCall)
case .toolCalls:
let toolCalls = try decoder.decode(ToolCalls.self, from: unescapedData)
event = Event.toolCalls(toolCalls.toolCalls)
case .hang:
event = Event.hang
case .transcript:
Expand Down