166 lines
5.9 KiB
Swift
166 lines
5.9 KiB
Swift
// Copyright © 2025 Apple Inc.
|
|
|
|
import Foundation
|
|
|
|
// MARK: - ToolCallParser Protocol
|
|
|
|
/// Protocol for parsing tool call content from model output.
|
|
///
|
|
/// Different models use different formats for tool calls. This protocol provides
|
|
/// a common interface for parsing tool calls from model output text.
|
|
///
|
|
/// Reference: https://github.com/ml-explore/mlx-lm/tree/main/mlx_lm/tool_parsers
|
|
public protocol ToolCallParser: Sendable {
|
|
/// The start tag that indicates a tool call is beginning.
|
|
/// Returns `nil` for inline formats that don't use wrapper tags.
|
|
var startTag: String? { get }
|
|
|
|
/// The end tag that indicates a tool call has ended.
|
|
/// Returns `nil` for inline formats that don't use wrapper tags.
|
|
var endTag: String? { get }
|
|
|
|
/// Parse the content into a `ToolCall`.
|
|
/// - Parameters:
|
|
/// - content: The text content to parse (may include tags)
|
|
/// - tools: Optional tool schemas for type-aware parsing
|
|
/// - Returns: A `ToolCall` if parsing succeeds, `nil` otherwise
|
|
func parse(content: String, tools: [[String: any Sendable]]?) -> ToolCall?
|
|
|
|
/// Parse remaining buffered content at end-of-sequence.
|
|
///
|
|
/// Called when generation ends to extract any tool calls still in the buffer.
|
|
/// The default implementation splits on `startTag` (if present) and parses
|
|
/// each segment individually.
|
|
func parseEOS(_ toolCallBuffer: String, tools: [[String: any Sendable]]?) -> [ToolCall]
|
|
}
|
|
|
|
extension ToolCallParser {
|
|
public func parseEOS(_ toolCallBuffer: String, tools: [[String: any Sendable]]?) -> [ToolCall] {
|
|
if let startTag {
|
|
return
|
|
toolCallBuffer
|
|
.components(separatedBy: startTag)
|
|
.filter { !$0.isEmpty }
|
|
.compactMap { parse(content: $0, tools: tools) }
|
|
} else {
|
|
guard let toolCall = parse(content: toolCallBuffer, tools: tools) else {
|
|
return []
|
|
}
|
|
return [toolCall]
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ToolCallFormat Enum
|
|
|
|
/// Supported tool call formats for different language models.
|
|
///
|
|
/// This enum defines the various tool call formats used by different LLM families.
|
|
/// Each format has its own syntax for encoding function names and arguments.
|
|
///
|
|
/// The raw string values can be used for JSON serialization or CLI parameters.
|
|
///
|
|
/// Reference: https://github.com/ml-explore/mlx-lm/tree/main/mlx_lm/tool_parsers
|
|
public enum ToolCallFormat: String, Sendable, Codable, CaseIterable {
|
|
/// Default JSON format used by Llama, Qwen, and most models.
|
|
/// Example: `<tool_call>{"name": "func", "arguments": {...}}</tool_call>`
|
|
case json
|
|
|
|
/// LFM2/LFM2.5 Pythonic format with model-specific tags.
|
|
/// Example: `<|tool_call_start|>[func(arg='value')]<|tool_call_end|>`
|
|
case lfm2
|
|
|
|
/// XML function format used by Nemotron, Qwen3 Coder, Qwen3.5, and similar models.
|
|
/// Example: `<tool_call><function=name><parameter=key>value</parameter></function></tool_call>`
|
|
case xmlFunction = "xml_function"
|
|
|
|
/// GLM4 format with arg_key/arg_value tags.
|
|
/// Example: `func<arg_key>k</arg_key><arg_value>v</arg_value>`
|
|
case glm4
|
|
|
|
/// Gemma function call format.
|
|
/// Example: `call:name{key:value,k:<escape>str<escape>}`
|
|
case gemma
|
|
|
|
/// Kimi K2 format with functions prefix.
|
|
/// Example: `functions.name:0<|tool_call_argument_begin|>{"key": "value"}`
|
|
case kimiK2 = "kimi_k2"
|
|
|
|
/// MiniMax M2 format with invoke/parameter tags.
|
|
/// Example: `<invoke name="f"><parameter name="k">v</parameter></invoke>`
|
|
case minimaxM2 = "minimax_m2"
|
|
|
|
/// Mistral V11+ format with [TOOL_CALLS] and [ARGS] delimiters.
|
|
/// Example: `[TOOL_CALLS]get_weather [ARGS]{"location": "Tokyo"}`
|
|
case mistral
|
|
|
|
// MARK: - Factory Methods
|
|
|
|
/// Create the appropriate parser for this format.
|
|
/// - Returns: A parser instance configured for this format
|
|
public func createParser() -> any ToolCallParser {
|
|
switch self {
|
|
case .json:
|
|
return JSONToolCallParser(startTag: "<tool_call>", endTag: "</tool_call>")
|
|
case .lfm2:
|
|
return PythonicToolCallParser(
|
|
startTag: "<|tool_call_start|>", endTag: "<|tool_call_end|>")
|
|
case .xmlFunction:
|
|
return XMLFunctionParser(startTag: "<tool_call>", endTag: "</tool_call>")
|
|
case .glm4:
|
|
return GLM4ToolCallParser()
|
|
case .gemma:
|
|
return GemmaFunctionParser()
|
|
case .kimiK2:
|
|
return KimiK2ToolCallParser()
|
|
case .minimaxM2:
|
|
return MiniMaxM2ToolCallParser()
|
|
case .mistral:
|
|
return MistralToolCallParser()
|
|
}
|
|
}
|
|
|
|
/// Infer the tool call format based on model type from config.json.
|
|
///
|
|
/// This method maps known model types to their corresponding tool call formats,
|
|
/// enabling automatic format detection when loading models.
|
|
///
|
|
/// - Parameter modelType: The `model_type` value from config.json
|
|
/// - Returns: The appropriate `ToolCallFormat`, or `nil` to use the default format
|
|
public static func infer(from modelType: String) -> ToolCallFormat? {
|
|
let type = modelType.lowercased()
|
|
|
|
// LFM2 family (lfm2, lfm2_moe, lfm2_5, lfm25, etc.)
|
|
if type.hasPrefix("lfm2") {
|
|
return .lfm2
|
|
}
|
|
|
|
// GLM4 family (glm4, glm4_moe, glm4_moe_lite, etc.)
|
|
if type.hasPrefix("glm4") {
|
|
return .glm4
|
|
}
|
|
|
|
// Gemma
|
|
if type == "gemma" {
|
|
return .gemma
|
|
}
|
|
|
|
// Nemotron family (nemotron_h, etc.)
|
|
if type.hasPrefix("nemotron") {
|
|
return .xmlFunction
|
|
}
|
|
|
|
// Qwen3.5 family (qwen3_5, qwen3_5_moe, etc.)
|
|
if type.hasPrefix("qwen3_5") {
|
|
return .xmlFunction
|
|
}
|
|
|
|
// Mistral3 family (mistral3, mistral3_text, etc.)
|
|
if type.hasPrefix("mistral3") {
|
|
return .mistral
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|