180 lines
7.1 KiB
Swift
180 lines
7.1 KiB
Swift
import Foundation
|
|
import XCTest
|
|
@testable import MLX_Server
|
|
|
|
final class LocalModelResolverTests: XCTestCase {
|
|
func testDiscoverModelsInfersTextOnlyMetadataAndDirectorySize() throws {
|
|
let base = try makeTempModelsRoot()
|
|
let repoDirectory = try makeRepoDirectory(base: base, owner: "example", repo: "text-only")
|
|
let configURL = repoDirectory.appendingPathComponent("config.json")
|
|
let modelURL = repoDirectory.appendingPathComponent("model.safetensors")
|
|
let tokenizerURL = repoDirectory.appendingPathComponent("tokenizer.json")
|
|
|
|
try writeJSON(
|
|
[
|
|
"architectures": ["LlamaForCausalLM"],
|
|
"max_position_embeddings": 32768,
|
|
],
|
|
to: configURL
|
|
)
|
|
try Data(repeating: 0x11, count: 64).write(to: modelURL)
|
|
try Data(repeating: 0x22, count: 19).write(to: tokenizerURL)
|
|
|
|
let expectedSize = Int64(
|
|
try Data(contentsOf: configURL).count
|
|
+ Data(contentsOf: modelURL).count
|
|
+ Data(contentsOf: tokenizerURL).count
|
|
)
|
|
|
|
let discovered = LocalModelResolver.discoverModels(in: base)
|
|
let model = try XCTUnwrap(discovered.first)
|
|
|
|
XCTAssertEqual(model.repoId, "example/text-only")
|
|
XCTAssertEqual(model.contextLength, 32768)
|
|
XCTAssertFalse(model.supportsImages)
|
|
XCTAssertEqual(model.loaderKinds, [.llm, .vlm])
|
|
XCTAssertEqual(model.sizeBytes, expectedSize)
|
|
}
|
|
|
|
func testDiscoverModelsInfersVisionMetadataFromProcessorFiles() throws {
|
|
let base = try makeTempModelsRoot()
|
|
let repoDirectory = try makeRepoDirectory(base: base, owner: "example", repo: "vision-model")
|
|
try writeJSON(
|
|
[
|
|
"text_config": ["max_position_embeddings": 262144],
|
|
"vision_config": ["hidden_size": 768],
|
|
],
|
|
to: repoDirectory.appendingPathComponent("config.json")
|
|
)
|
|
try writeJSON(["processor_class": "Qwen3VLProcessor"], to: repoDirectory.appendingPathComponent("tokenizer_config.json"))
|
|
try Data(repeating: 0x33, count: 12).write(to: repoDirectory.appendingPathComponent("processor_config.json"))
|
|
try Data(repeating: 0x44, count: 8).write(to: repoDirectory.appendingPathComponent("model.safetensors.index.json"))
|
|
|
|
let discovered = LocalModelResolver.discoverModels(in: base)
|
|
let model = try XCTUnwrap(discovered.first)
|
|
|
|
XCTAssertEqual(model.repoId, "example/vision-model")
|
|
XCTAssertEqual(model.contextLength, 262144)
|
|
XCTAssertTrue(model.supportsImages)
|
|
XCTAssertEqual(model.loaderKinds, [.vlm, .llm])
|
|
}
|
|
|
|
func testMergedCatalogKeepsCuratedModelsAndAddsCustomLocalModels() {
|
|
let localModels = [
|
|
LocalModelResolver.LocalModelInfo(
|
|
repoId: "mlx-community/gemma-3-4b-it-4bit",
|
|
directory: URL(fileURLWithPath: "/tmp/gemma"),
|
|
sizeBytes: 1024,
|
|
contextLength: 128000,
|
|
loaderKinds: [.vlm, .llm],
|
|
supportsImages: true
|
|
),
|
|
LocalModelResolver.LocalModelInfo(
|
|
repoId: "custom-org/custom-model",
|
|
directory: URL(fileURLWithPath: "/tmp/custom"),
|
|
sizeBytes: 2048,
|
|
contextLength: 65536,
|
|
loaderKinds: [.llm, .vlm],
|
|
supportsImages: false
|
|
),
|
|
]
|
|
|
|
let merged = ModelConfig.mergedModels(localModels: localModels)
|
|
let gemma = merged.first(where: { $0.id == "gemma" })
|
|
let custom = merged.first(where: { $0.repoId == "custom-org/custom-model" })
|
|
|
|
XCTAssertEqual(gemma?.localSizeBytes, 1024)
|
|
XCTAssertEqual(custom?.id, "custom-org/custom-model")
|
|
XCTAssertEqual(custom?.contextLength, 65536)
|
|
XCTAssertFalse(custom?.isCurated ?? true)
|
|
}
|
|
|
|
func testResolveUnknownRepoIdCreatesRemoteCustomConfig() throws {
|
|
let config = try XCTUnwrap(ModelConfig.resolve("custom-owner/custom-repo"))
|
|
|
|
XCTAssertEqual(config.id, "custom-owner/custom-repo")
|
|
XCTAssertEqual(config.repoId, "custom-owner/custom-repo")
|
|
XCTAssertFalse(config.isCurated)
|
|
}
|
|
|
|
func testMergedCatalogAppliesSavedMetadataOverride() {
|
|
let repoId = "custom-org/override-model"
|
|
Preferences.setModelMetadataOverride(
|
|
ModelMetadataOverride(
|
|
contextLength: 123456,
|
|
primaryLoaderKind: .vlm,
|
|
supportsImages: true,
|
|
supportsTools: true
|
|
),
|
|
forRepoId: repoId
|
|
)
|
|
defer {
|
|
Preferences.removeModelMetadataOverride(forRepoId: repoId)
|
|
}
|
|
|
|
let localModels = [
|
|
LocalModelResolver.LocalModelInfo(
|
|
repoId: repoId,
|
|
directory: URL(fileURLWithPath: "/tmp/custom-override"),
|
|
sizeBytes: 2048,
|
|
contextLength: 65536,
|
|
loaderKinds: [.llm, .vlm],
|
|
supportsImages: false
|
|
),
|
|
]
|
|
|
|
let merged = ModelConfig.mergedModels(localModels: localModels)
|
|
let overridden = merged.first(where: { $0.repoId == repoId })
|
|
|
|
XCTAssertEqual(overridden?.contextLength, 123456)
|
|
XCTAssertEqual(overridden?.primaryLoaderKind, .vlm)
|
|
XCTAssertTrue(overridden?.supportsImages ?? false)
|
|
XCTAssertTrue(overridden?.supportsTools ?? false)
|
|
}
|
|
|
|
func testResolveUnknownRepoIdUsesSavedMetadataOverride() throws {
|
|
let repoId = "custom-owner/custom-repo-with-override"
|
|
Preferences.setModelMetadataOverride(
|
|
ModelMetadataOverride(
|
|
contextLength: 8192,
|
|
primaryLoaderKind: .llm,
|
|
supportsImages: false,
|
|
supportsTools: true
|
|
),
|
|
forRepoId: repoId
|
|
)
|
|
defer {
|
|
Preferences.removeModelMetadataOverride(forRepoId: repoId)
|
|
}
|
|
|
|
let config = try XCTUnwrap(ModelConfig.resolve(repoId))
|
|
|
|
XCTAssertEqual(config.contextLength, 8192)
|
|
XCTAssertEqual(config.primaryLoaderKind, .llm)
|
|
XCTAssertFalse(config.supportsImages)
|
|
XCTAssertTrue(config.supportsTools)
|
|
}
|
|
|
|
private func makeTempModelsRoot() throws -> URL {
|
|
let root = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
addTeardownBlock {
|
|
try? FileManager.default.removeItem(at: root)
|
|
}
|
|
return root
|
|
}
|
|
|
|
private func makeRepoDirectory(base: URL, owner: String, repo: String) throws -> URL {
|
|
let directory = base
|
|
.appendingPathComponent(owner, isDirectory: true)
|
|
.appendingPathComponent(repo, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
return directory
|
|
}
|
|
|
|
private func writeJSON(_ object: Any, to url: URL) throws {
|
|
let data = try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys])
|
|
try data.write(to: url)
|
|
}
|
|
} |