Skip to content

Commit 63a5cf8

Browse files
committed
Map ChatCompletionRequest tool_calls natively into Chat.Message to retain contextual history
1 parent 2955afa commit 63a5cf8

1 file changed

Lines changed: 110 additions & 19 deletions

File tree

‎Sources/SwiftLM/Server.swift‎

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,61 @@ import CoreImage
1414
import Foundation
1515
import HTTPTypes
1616
import Hummingbird
17+
import Hub
1718
import MLX
1819
import MLXLLM
1920
import MLXLMCommon
2021
import MLXVLM
22+
import Tokenizers
23+
24+
// ── Hub/Tokenizer bridges (Downloader + TokenizerLoader conformances) ─────────
25+
26+
private struct HubDownloader: Downloader, Sendable {
27+
let hub: HubApi
28+
func download(
29+
id: String, revision: String?,
30+
matching patterns: [String],
31+
useLatest: Bool,
32+
progressHandler: @Sendable @escaping (Progress) -> Void
33+
) async throws -> URL {
34+
try await hub.snapshot(from: id, matching: patterns, progressHandler: progressHandler)
35+
}
36+
}
37+
38+
private struct TransformersTokenizerLoader: TokenizerLoader, Sendable {
39+
func load(from directory: URL) async throws -> any MLXLMCommon.Tokenizer {
40+
let t = try await AutoTokenizer.from(modelFolder: directory)
41+
return TransformersTokenizerBridge(t)
42+
}
43+
}
44+
45+
private struct TransformersTokenizerBridge: MLXLMCommon.Tokenizer, Sendable {
46+
let upstream: any Tokenizers.Tokenizer
47+
init(_ upstream: any Tokenizers.Tokenizer) { self.upstream = upstream }
48+
func encode(text: String, addSpecialTokens: Bool) -> [Int] {
49+
upstream.encode(text: text, addSpecialTokens: addSpecialTokens)
50+
}
51+
func decode(tokenIds: [Int], skipSpecialTokens: Bool) -> String {
52+
upstream.decode(tokens: tokenIds, skipSpecialTokens: skipSpecialTokens)
53+
}
54+
func convertTokenToId(_ token: String) -> Int? { upstream.convertTokenToId(token) }
55+
func convertIdToToken(_ id: Int) -> String? { upstream.convertIdToToken(id) }
56+
var bosToken: String? { upstream.bosToken }
57+
var eosToken: String? { upstream.eosToken }
58+
var unknownToken: String? { upstream.unknownToken }
59+
func applyChatTemplate(
60+
messages: [[String: any Sendable]],
61+
tools: [[String: any Sendable]]?,
62+
additionalContext: [String: any Sendable]?
63+
) throws -> [Int] {
64+
do {
65+
return try upstream.applyChatTemplate(
66+
messages: messages, tools: tools, additionalContext: additionalContext)
67+
} catch Tokenizers.TokenizerError.missingChatTemplate {
68+
throw MLXLMCommon.TokenizerError.missingChatTemplate
69+
}
70+
}
71+
}
2172

2273
// ── CLI ──────────────────────────────────────────────────────────────────────
2374

@@ -332,21 +383,32 @@ struct MLXServer: AsyncParsableCommand {
332383
}()
333384
let tracker = ProgressTracker(modelId: resolvedModelId)
334385

386+
let cacheRoot = URL.applicationSupportDirectory
387+
.appendingPathComponent("MLX", isDirectory: true)
388+
.appendingPathComponent("HuggingFace", isDirectory: true)
335389
if isVision {
336390
print("[SwiftLM] Loading VLM (vision-language model)...")
391+
let downloader = HubDownloader(hub: HubApi(downloadBase: cacheRoot))
337392
container = try await VLMModelFactory.shared.loadContainer(
393+
from: downloader,
394+
using: TransformersTokenizerLoader(),
338395
configuration: modelConfig
339396
) { progress in
340397
tracker.printProgress(progress)
341398
}
342399
} else {
400+
let downloader = HubDownloader(hub: HubApi(downloadBase: cacheRoot))
343401
container = try await LLMModelFactory.shared.loadContainer(
402+
from: downloader,
403+
using: TransformersTokenizerLoader(),
344404
configuration: modelConfig
345405
) { progress in
346406
tracker.printProgress(progress)
347407
}
348408
}
349409

410+
print("[SwiftLM] Loaded model configuration. Inferred tool call format: \(String(describing: await container.configuration.toolCallFormat))")
411+
350412
// ── Apply GPU/CPU layer partitioning ──
351413
if let gpuCount = requestedGPULayers {
352414
let actual = await container.setGPULayers(gpuCount)
@@ -914,7 +976,22 @@ func handleChatCompletion(
914976
chatMessages.append(.system(textContent, images: images))
915977
systemPromptText += textContent
916978
case "assistant":
917-
chatMessages.append(.assistant(textContent, images: images))
979+
var formattedToolCalls: [[String: any Sendable]]? = nil
980+
if let tc = msg.tool_calls, !tc.isEmpty {
981+
formattedToolCalls = tc.map { call in
982+
[
983+
"id": call.id,
984+
"type": call.type,
985+
"function": [
986+
"name": call.function.name,
987+
"arguments": call.function.arguments
988+
] as [String: any Sendable]
989+
] as [String: any Sendable]
990+
}
991+
}
992+
chatMessages.append(.assistant(textContent, images: images, toolCalls: formattedToolCalls))
993+
case "tool":
994+
chatMessages.append(.tool(textContent, toolCallId: msg.tool_call_id))
918995
default:
919996
chatMessages.append(.user(textContent, images: images))
920997
}
@@ -1885,6 +1962,8 @@ struct ChatCompletionRequest: Decodable {
18851962
struct Message: Decodable {
18861963
let role: String
18871964
let content: MessageContent?
1965+
let tool_calls: [ToolCallResponse]?
1966+
let tool_call_id: String?
18881967

18891968
/// Extract plain text from content (handles both string and multipart)
18901969
var textContent: String {
@@ -1932,11 +2011,11 @@ struct ChatCompletionRequest: Decodable {
19322011
case string(String)
19332012
case parts([ContentPart])
19342013

1935-
init(from decoder: Decoder) throws {
1936-
let container = try decoder.singleValueContainer()
1937-
if let str = try? container.decode(String.self) {
2014+
init(from decoder: Swift.Decoder) throws {
2015+
let svc = try decoder.singleValueContainer()
2016+
if let str = try? svc.decode(String.self) {
19382017
self = .string(str)
1939-
} else if let parts = try? container.decode([ContentPart].self) {
2018+
} else if let parts = try? svc.decode([ContentPart].self) {
19402019
self = .parts(parts)
19412020
} else {
19422021
self = .string("")
@@ -2065,13 +2144,13 @@ struct AssistantMessage: Encodable {
20652144
}
20662145
}
20672146

2068-
struct ToolCallResponse: Encodable {
2147+
struct ToolCallResponse: Codable {
20692148
let id: String
20702149
let type: String
20712150
let function: ToolCallFunction
20722151
}
20732152

2074-
struct ToolCallFunction: Encodable {
2153+
struct ToolCallFunction: Codable {
20752154
let name: String
20762155
let arguments: String
20772156
}
@@ -2096,25 +2175,37 @@ struct TextChoice: Encodable {
20962175
}
20972176
}
20982177

2099-
struct AnyCodable: Decodable, Sendable {
2178+
// AnyCodable: a Sendable-compatible type-erased JSON value.
2179+
// `value` stores only Sendable-compatible Foundation types: Bool, Int, Double,
2180+
// String, NSNull, [AnyCodable.SendableValue], [String: AnyCodable.SendableValue]
2181+
// AnyCodable: a type-erased JSON value that bridges to Foundation types.
2182+
// Marked @unchecked Sendable: all stored types (Bool/Int/Double/String/NSNull/
2183+
// recursive AnyCodable) are in fact Sendable; `Any` is used for ergonomic storage.
2184+
// AnyCodable: type-erased Decodable wrapper over JSON scalars/arrays/objects.
2185+
// `value` holds Sendable-safe Foundation types (Bool/Int/Double/String/NSNull + collections).
2186+
struct AnyCodable: @unchecked Sendable {
21002187
let value: Any
2101-
init(from decoder: Decoder) throws {
2102-
let c = try decoder.singleValueContainer()
2103-
if c.decodeNil() { value = NSNull() }
2104-
else if let b = try? c.decode(Bool.self) { value = b }
2105-
else if let i = try? c.decode(Int.self) { value = i }
2106-
else if let d = try? c.decode(Double.self) { value = d }
2107-
else if let s = try? c.decode(String.self) { value = s }
2108-
else if let a = try? c.decode([AnyCodable].self) { value = a.map { $0.value } }
2109-
else if let d = try? c.decode([String: AnyCodable].self) { value = d.mapValues { $0.value } }
2110-
else { value = NSNull() }
2111-
}
2188+
21122189
static func toSendable(_ dict: [String: AnyCodable]?) -> [String: any Sendable]? {
21132190
guard let dict else { return nil }
21142191
return dict.mapValues { $0.value as! any Sendable }
21152192
}
21162193
}
21172194

2195+
extension AnyCodable: Decodable {
2196+
init(from decoder: Swift.Decoder) throws {
2197+
let c = try decoder.singleValueContainer()
2198+
if (try? c.decodeNil()) == true { value = NSNull(); return }
2199+
if let b = try? c.decode(Bool.self) { value = b; return }
2200+
if let i = try? c.decode(Int.self) { value = i; return }
2201+
if let d = try? c.decode(Double.self) { value = d; return }
2202+
if let s = try? c.decode(String.self) { value = s; return }
2203+
if let a = try? c.decode([AnyCodable].self) { value = a.map { $0.value }; return }
2204+
if let o = try? c.decode([String: AnyCodable].self) { value = o.mapValues { $0.value }; return }
2205+
value = NSNull()
2206+
}
2207+
}
2208+
21182209
struct TokenUsage: Encodable {
21192210
let promptTokens: Int
21202211
let completionTokens: Int

0 commit comments

Comments
 (0)