Skip to content

Commit 2b855bb

Browse files
committed
Improve error handling
1 parent c5725ae commit 2b855bb

7 files changed

Lines changed: 121 additions & 17 deletions

File tree

Libraries/Embedders/Configuration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private class ModelTypeRegistry: @unchecked Sendable {
7373
creators[rawValue]
7474
}
7575
guard let creator else {
76-
throw EmbedderError(message: "Unsupported model type.")
76+
throw EmbedderError.unsupportedModelType(rawValue)
7777
}
7878
return try creator(configuration)
7979
}

Libraries/Embedders/EmbeddingModel.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ public actor ModelContainer {
5151
async let tokenizerConfigTask = loadTokenizerConfig(
5252
configuration: configuration, hub: hub)
5353

54-
self.model = try loadSynchronous(modelDirectory: modelDirectory)
54+
self.model = try loadSynchronous(
55+
modelDirectory: modelDirectory, modelName: configuration.name)
5556
self.pooler = loadPooling(modelDirectory: modelDirectory)
5657

5758
let (tokenizerConfig, tokenizerData) = try await tokenizerConfigTask

Libraries/Embedders/Load.swift

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,51 @@ import MLX
66
import MLXNN
77
import Tokenizers
88

9-
struct EmbedderError: Error {
10-
let message: String
9+
public enum EmbedderError: LocalizedError {
10+
case unsupportedModelType(String)
11+
case missingConfigurationFile(String, String)
12+
case configurationFileError(String, String, Error)
13+
case configurationDecodingError(String, String, DecodingError)
14+
case missingTokenizerConfig
15+
16+
public var errorDescription: String? {
17+
switch self {
18+
case .unsupportedModelType(let type):
19+
return "Unsupported model type: \(type)"
20+
case .missingConfigurationFile(let file, let modelName):
21+
return "Missing or unreadable configuration file '\(file)' for model '\(modelName)'"
22+
case .configurationFileError(let file, let modelName, let error):
23+
return "Error reading '\(file)' for model '\(modelName)': \(error.localizedDescription)"
24+
case .configurationDecodingError(let file, let modelName, let decodingError):
25+
let errorDetail = extractDecodingErrorDetail(decodingError)
26+
return "Failed to parse \(file) for model '\(modelName)': \(errorDetail)"
27+
case .missingTokenizerConfig:
28+
return "Missing tokenizer configuration"
29+
}
30+
}
31+
32+
private func extractDecodingErrorDetail(_ error: DecodingError) -> String {
33+
switch error {
34+
case .keyNotFound(let key, let context):
35+
let path = (context.codingPath + [key]).map { $0.stringValue }.joined(separator: ".")
36+
return "Missing field '\(path)'"
37+
case .typeMismatch(_, let context):
38+
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
39+
return "Type mismatch at '\(path)'"
40+
case .valueNotFound(_, let context):
41+
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
42+
return "Missing value at '\(path)'"
43+
case .dataCorrupted(let context):
44+
if context.codingPath.isEmpty {
45+
return "Invalid JSON"
46+
} else {
47+
let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
48+
return "Invalid data at '\(path)'"
49+
}
50+
@unknown default:
51+
return error.localizedDescription
52+
}
53+
}
1154
}
1255

1356
func prepareModelDirectory(
@@ -53,20 +96,38 @@ public func load(
5396
// Start tokenizer loading asynchronously, then load model synchronously.
5497
// Both operations run in parallel because async let begins execution immediately.
5598
async let tokenizerTask = loadTokenizer(configuration: configuration, hub: hub)
56-
let model = try loadSynchronous(modelDirectory: modelDirectory)
99+
let model = try loadSynchronous(modelDirectory: modelDirectory, modelName: configuration.name)
57100
let tokenizer = try await tokenizerTask
58101

59102
return (model, tokenizer)
60103
}
61104

62-
func loadSynchronous(modelDirectory: URL) throws -> EmbeddingModel {
105+
func loadSynchronous(modelDirectory: URL, modelName: String) throws -> EmbeddingModel {
63106
// Load config.json once and decode for both base config and model-specific config
64107
let configurationURL = modelDirectory.appending(component: "config.json")
65-
let configData = try Data(contentsOf: configurationURL)
66-
let baseConfig = try JSONDecoder().decode(BaseConfiguration.self, from: configData)
108+
let configData: Data
109+
do {
110+
configData = try Data(contentsOf: configurationURL)
111+
} catch {
112+
throw EmbedderError.configurationFileError(
113+
configurationURL.lastPathComponent, modelName, error)
114+
}
115+
let baseConfig: BaseConfiguration
116+
do {
117+
baseConfig = try JSONDecoder().decode(BaseConfiguration.self, from: configData)
118+
} catch let error as DecodingError {
119+
throw EmbedderError.configurationDecodingError(
120+
configurationURL.lastPathComponent, modelName, error)
121+
}
67122

68123
let modelType = ModelType(rawValue: baseConfig.modelType)
69-
let model = try modelType.createModel(configuration: configData)
124+
let model: EmbeddingModel
125+
do {
126+
model = try modelType.createModel(configuration: configData)
127+
} catch let error as DecodingError {
128+
throw EmbedderError.configurationDecodingError(
129+
configurationURL.lastPathComponent, modelName, error)
130+
}
70131

71132
// load the weights
72133
var weights = [String: MLXArray]()

Libraries/Embedders/Tokenizer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func loadTokenizerConfig(configuration: ModelConfiguration, hub: HubApi) async t
4545
}
4646

4747
guard let tokenizerConfig = try await config.tokenizerConfig else {
48-
throw EmbedderError(message: "missing config")
48+
throw EmbedderError.missingTokenizerConfig
4949
}
5050
let tokenizerData = try await config.tokenizerData
5151
return (tokenizerConfig, tokenizerData)

Libraries/MLXLLM/LLMModelFactory.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,9 +480,14 @@ public final class LLMModelFactory: ModelFactory {
480480
// Load config.json once and decode for both base config and model-specific config
481481
let configurationURL = modelDirectory.appending(component: "config.json")
482482
let configData: Data
483-
let baseConfig: BaseConfiguration
484483
do {
485484
configData = try Data(contentsOf: configurationURL)
485+
} catch {
486+
throw ModelFactoryError.configurationFileError(
487+
configurationURL.lastPathComponent, configuration.name, error)
488+
}
489+
let baseConfig: BaseConfiguration
490+
do {
486491
baseConfig = try JSONDecoder().decode(BaseConfiguration.self, from: configData)
487492
} catch let error as DecodingError {
488493
throw ModelFactoryError.configurationDecodingError(

Libraries/MLXLMCommon/ModelFactory.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Tokenizers
77
public enum ModelFactoryError: LocalizedError {
88
case unsupportedModelType(String)
99
case unsupportedProcessorType(String)
10+
case missingConfigurationFile(String, String)
11+
case configurationFileError(String, String, Error)
1012
case configurationDecodingError(String, String, DecodingError)
1113
case noModelFactoryAvailable
1214

@@ -16,6 +18,10 @@ public enum ModelFactoryError: LocalizedError {
1618
return "Unsupported model type: \(type)"
1719
case .unsupportedProcessorType(let type):
1820
return "Unsupported processor type: \(type)"
21+
case .missingConfigurationFile(let file, let modelName):
22+
return "Missing or unreadable configuration file '\(file)' for model '\(modelName)'"
23+
case .configurationFileError(let file, let modelName, let error):
24+
return "Error reading '\(file)' for model '\(modelName)': \(error.localizedDescription)"
1925
case .noModelFactoryAvailable:
2026
return "No model factory available via ModelFactoryRegistry"
2127
case .configurationDecodingError(let file, let modelName, let decodingError):

Libraries/MLXVLM/VLMModelFactory.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,14 @@ public final class VLMModelFactory: ModelFactory {
259259
// Load config.json once and decode for both base config and model-specific config
260260
let configurationURL = modelDirectory.appending(component: "config.json")
261261
let configData: Data
262-
let baseConfig: BaseConfiguration
263262
do {
264263
configData = try Data(contentsOf: configurationURL)
264+
} catch {
265+
throw ModelFactoryError.configurationFileError(
266+
configurationURL.lastPathComponent, configuration.name, error)
267+
}
268+
let baseConfig: BaseConfiguration
269+
do {
265270
baseConfig = try JSONDecoder().decode(BaseConfiguration.self, from: configData)
266271
} catch let error as DecodingError {
267272
throw ModelFactoryError.configurationDecodingError(
@@ -287,7 +292,20 @@ public final class VLMModelFactory: ModelFactory {
287292
perLayerQuantization: baseConfig.perLayerQuantization)
288293

289294
let tokenizer = try await tokenizerTask
290-
let (processorConfigData, baseProcessorConfig) = try await processorConfigTask
295+
let processorConfigData: Data
296+
let baseProcessorConfig: BaseProcessorConfiguration
297+
let processorConfigFilename: String
298+
do {
299+
(processorConfigData, baseProcessorConfig, processorConfigFilename) =
300+
try await processorConfigTask
301+
} catch let error as ProcessorConfigError {
302+
if let decodingError = error.underlying as? DecodingError {
303+
throw ModelFactoryError.configurationDecodingError(
304+
error.filename, configuration.name, decodingError)
305+
}
306+
throw ModelFactoryError.configurationFileError(
307+
error.filename, configuration.name, error.underlying)
308+
}
291309

292310
// Override processor type based on model type for models that need special handling
293311
// Mistral3 models ship with "PixtralProcessor" in their config but need Mistral3Processor
@@ -308,19 +326,32 @@ public final class VLMModelFactory: ModelFactory {
308326

309327
}
310328

329+
/// Error wrapper that includes the filename for better error messages.
330+
private struct ProcessorConfigError: Error {
331+
let filename: String
332+
let underlying: Error
333+
}
334+
311335
/// Loads processor configuration, preferring preprocessor_config.json over processor_config.json.
336+
/// Returns the data, decoded config, and the filename that was loaded.
337+
/// Throws ProcessorConfigError wrapping any underlying error with the filename.
312338
private func loadProcessorConfig(from modelDirectory: URL) async throws -> (
313-
Data, BaseProcessorConfiguration
339+
Data, BaseProcessorConfiguration, String
314340
) {
315341
let processorConfigURL = modelDirectory.appending(component: "processor_config.json")
316342
let preprocessorConfigURL = modelDirectory.appending(component: "preprocessor_config.json")
317343
let url =
318344
FileManager.default.fileExists(atPath: preprocessorConfigURL.path)
319345
? preprocessorConfigURL
320346
: processorConfigURL
321-
let data = try Data(contentsOf: url)
322-
let config = try JSONDecoder().decode(BaseProcessorConfiguration.self, from: data)
323-
return (data, config)
347+
let filename = url.lastPathComponent
348+
do {
349+
let data = try Data(contentsOf: url)
350+
let config = try JSONDecoder().decode(BaseProcessorConfiguration.self, from: data)
351+
return (data, config, filename)
352+
} catch {
353+
throw ProcessorConfigError(filename: filename, underlying: error)
354+
}
324355
}
325356

326357
public class TrampolineModelFactory: NSObject, ModelFactoryTrampoline {

0 commit comments

Comments
 (0)