From f8ae6fad4b4065aa8f69b2098b08e23bead9687d Mon Sep 17 00:00:00 2001 From: Priyal Chhatrapati Date: Wed, 17 Jun 2026 19:06:05 -0700 Subject: [PATCH] Gracefully exit when pointed at a model asset instead of a model bundle directory When llm-runner / llm-benchmark were pointed at a .aimodel or .aimodelc asset instead of the parent model bundle directory, they failed with a confusing error. A compiled .aimodelc is itself a directory containing its own unrelated metadata.json, so ModelBundle parsed it, defaulted it to metadata_version "0.1", and surfaced the misleading "unsupported metadata_version '0.1'". A .aimodel path instead produced "metadata.json not found at .../model.aimodel/metadata.json". Detect a .aimodel/.aimodelc path extension at the top of ModelBundle.init(at:), before any filesystem read, and throw a clear new BundleError.pointedAtModelAsset. The CLIs already catch BundleError and exit non-zero, so all tools that load through ModelBundle (llm-runner, llm-benchmark, VLM dispatch) now exit gracefully. Also align llm-benchmark's --model help text with llm-runner's ("Path to a model bundle directory"). --- .../CoreAIShared/Bundle/ModelBundle.swift | 15 ++++++++ .../Tools/benchmark/BenchmarkMain.swift | 2 +- .../CoreAISharedTests/ModelBundleTests.swift | 36 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/swift/Sources/CoreAIShared/Bundle/ModelBundle.swift b/swift/Sources/CoreAIShared/Bundle/ModelBundle.swift index 155a786..e25b9a8 100644 --- a/swift/Sources/CoreAIShared/Bundle/ModelBundle.swift +++ b/swift/Sources/CoreAIShared/Bundle/ModelBundle.swift @@ -82,9 +82,14 @@ public struct ModelBundle: Sendable { case kindMismatch(expected: BundleKind, got: BundleKind) case missingField(String) case missingAsset(key: String, path: URL) + case pointedAtModelAsset(URL) public var description: String { switch self { + case .pointedAtModelAsset(let url): + return "'\(url.lastPathComponent)' is a model asset, not a model bundle " + + "directory. A model bundle directory contains metadata, a tokenizer, " + + "and a model asset." case .missingMetadata(let url): return "metadata.json not found at \(url.path)" case .malformedMetadata(let url, let err): @@ -114,6 +119,16 @@ public struct ModelBundle: Sendable { } public init(at url: URL) throws { + // A model bundle is a *directory* (metadata.json + assets + tokenizer). + // If the caller points us directly at a `.aimodel`/`.aimodelc` asset, + // fail with actionable guidance. This must run before any filesystem + // read: a compiled `.aimodelc` is itself a directory holding its own + // unrelated metadata.json, which would otherwise parse as a bogus 0.1 + // bundle and surface a misleading "unsupported metadata_version" error. + let ext = url.pathExtension.lowercased() + if ext == "aimodel" || ext == "aimodelc" { + throw BundleError.pointedAtModelAsset(url) + } let metadataURL = url.appending(path: "metadata.json") guard FileManager.default.fileExists(atPath: metadataURL.path) else { throw BundleError.missingMetadata(metadataURL) diff --git a/swift/Sources/Tools/benchmark/BenchmarkMain.swift b/swift/Sources/Tools/benchmark/BenchmarkMain.swift index 47726f0..1e855e7 100644 --- a/swift/Sources/Tools/benchmark/BenchmarkMain.swift +++ b/swift/Sources/Tools/benchmark/BenchmarkMain.swift @@ -22,7 +22,7 @@ struct LLMBenchmark: AsyncParsableCommand { abstract: "LLM inference benchmark for CoreAI models" ) - @Option(name: .customLong("model"), help: "Path to model bundle directory") + @Option(name: .customLong("model"), help: "Path to a model bundle directory") var model: String @Option(name: [.customShort("p"), .customLong("prompt-tokens")], help: "Length of prompt") diff --git a/swift/Tests/CoreAISharedTests/ModelBundleTests.swift b/swift/Tests/CoreAISharedTests/ModelBundleTests.swift index 9a05013..793855d 100644 --- a/swift/Tests/CoreAISharedTests/ModelBundleTests.swift +++ b/swift/Tests/CoreAISharedTests/ModelBundleTests.swift @@ -78,4 +78,40 @@ struct ModelBundleTests { _ = try ModelBundle(at: dir) } } + + @Test("Pointing at a .aimodelc asset throws pointedAtModelAsset, not a parse error") + func pointedAtCompiledAssetThrows() throws { + // A compiled `.aimodelc` is a directory with its own unrelated + // metadata.json. Pointing the tool at it must fail fast with guidance, + // not parse that inner metadata as a bogus 0.1 bundle. + let bundleDir = FileManager.default.temporaryDirectory.appending( + path: "ModelBundleTests-\(UUID().uuidString)" + ) + let asset = bundleDir.appending(path: "model.aimodelc") + try FileManager.default.createDirectory(at: asset, withIntermediateDirectories: true) + try """ + { "producer": "coreai-build", "assetVersion": "2.0" } + """.write( + to: asset.appending(path: "metadata.json"), atomically: true, encoding: .utf8) + + let error = #expect(throws: ModelBundle.BundleError.self) { + _ = try ModelBundle(at: asset) + } + guard case .pointedAtModelAsset = error else { + Issue.record("expected pointedAtModelAsset, got \(String(describing: error))") + return + } + #expect(String(describing: error).contains("model.aimodelc")) + } + + @Test("Pointing at a .aimodel asset throws pointedAtModelAsset") + func pointedAtUncompiledAssetThrows() throws { + let error = #expect(throws: ModelBundle.BundleError.self) { + _ = try ModelBundle(from: "/some/where/model.aimodel") + } + guard case .pointedAtModelAsset = error else { + Issue.record("expected pointedAtModelAsset, got \(String(describing: error))") + return + } + } }