ac75faa0cc
CI / build-and-test (push) Has been cancelled
- E4B-MarkBase model (42 layers, 4.4GB) loaded successfully - All Phase 1-6 tests passed (model loading, forward pass, vision/audio towers, token generation, performance) - All stress tests passed (5/5 in 127.6s) - Concurrent inference - Memory stress (67.5 tok/s, 0 NaN) - Continuous generation - Batch processing - Long-running stability - Swift Metal inference engine with multimodal support
212 lines
6.6 KiB
Swift
212 lines
6.6 KiB
Swift
import Foundation
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Model Finder - Search and Select Models
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/// Model search result
|
|
public struct ModelSearchResult: Codable, Sendable {
|
|
public let id: String
|
|
public let modelId: String
|
|
public let author: String
|
|
public let sha: String?
|
|
public let lastModified: String?
|
|
public let isPrivate: Bool
|
|
public let disabled: Bool
|
|
public let gated: String?
|
|
public let pipelineTag: String?
|
|
public let tags: [String]
|
|
public let downloads: Int
|
|
public let likes: Int
|
|
public let libraryName: String?
|
|
public let createdAt: String?
|
|
|
|
public var displayName: String {
|
|
modelId.split(separator: "/").last.map(String.init) ?? modelId
|
|
}
|
|
|
|
public var isGGUF: Bool {
|
|
tags.contains { $0.lowercased().contains("gguf") }
|
|
}
|
|
|
|
public var isSafetensors: Bool {
|
|
tags.contains { $0.lowercased().contains("safetensors") }
|
|
}
|
|
}
|
|
|
|
/// Model search filters
|
|
public struct ModelFilters {
|
|
public var search: String?
|
|
public var author: String?
|
|
public var library: String?
|
|
public var task: String?
|
|
public var tags: [String]?
|
|
public var sort: String?
|
|
public var direction: Int? // -1 for descending, 1 for ascending
|
|
public var limit: Int?
|
|
|
|
public init(
|
|
search: String? = nil,
|
|
author: String? = nil,
|
|
library: String? = nil,
|
|
task: String? = nil,
|
|
tags: [String]? = nil,
|
|
sort: String? = nil,
|
|
direction: Int? = nil,
|
|
limit: Int? = nil
|
|
) {
|
|
self.search = search
|
|
self.author = author
|
|
self.library = library
|
|
self.task = task
|
|
self.tags = tags
|
|
self.sort = sort
|
|
self.direction = direction
|
|
self.limit = limit
|
|
}
|
|
|
|
/// Convert to query parameters
|
|
public func toQueryItems() -> [URLQueryItem] {
|
|
var items: [URLQueryItem] = []
|
|
|
|
if let search = search {
|
|
items.append(URLQueryItem(name: "search", value: search))
|
|
}
|
|
if let author = author {
|
|
items.append(URLQueryItem(name: "author", value: author))
|
|
}
|
|
if let library = library {
|
|
items.append(URLQueryItem(name: "filter", value: library))
|
|
}
|
|
if let task = task {
|
|
items.append(URLQueryItem(name: "pipeline_tag", value: task))
|
|
}
|
|
if let tags = tags {
|
|
for tag in tags {
|
|
items.append(URLQueryItem(name: "filter", value: tag))
|
|
}
|
|
}
|
|
if let sort = sort {
|
|
items.append(URLQueryItem(name: "sort", value: sort))
|
|
}
|
|
if let direction = direction {
|
|
items.append(URLQueryItem(name: "direction", value: "\(direction)"))
|
|
}
|
|
if let limit = limit {
|
|
items.append(URLQueryItem(name: "limit", value: "\(limit)"))
|
|
}
|
|
|
|
return items
|
|
}
|
|
}
|
|
|
|
/// Model finder
|
|
public final class ModelFinder {
|
|
public nonisolated(unsafe) static let shared = ModelFinder()
|
|
|
|
private let session: URLSession
|
|
|
|
private init() {
|
|
let config = URLSessionConfiguration.default
|
|
config.timeoutIntervalForRequest = 30
|
|
self.session = URLSession(configuration: config)
|
|
}
|
|
|
|
/// Search models on HuggingFace
|
|
public func searchModels(filters: ModelFilters) async throws -> [ModelSearchResult] {
|
|
var components = URLComponents(string: "https://huggingface.co/api/models")!
|
|
components.queryItems = filters.toQueryItems()
|
|
|
|
guard let url = components.url else {
|
|
throw ModelFinderError.invalidURL
|
|
}
|
|
|
|
let (data, _) = try await session.data(from: url)
|
|
let models = try JSONDecoder().decode([ModelSearchResult].self, from: data)
|
|
|
|
return models
|
|
}
|
|
|
|
/// Search by name
|
|
public func searchByName(_ name: String, limit: Int = 20) async throws -> [ModelSearchResult] {
|
|
let filters = ModelFilters(
|
|
search: name,
|
|
sort: "downloads",
|
|
direction: -1,
|
|
limit: limit
|
|
)
|
|
return try await searchModels(filters: filters)
|
|
}
|
|
|
|
/// Search GGUF models
|
|
public func searchGGUF(
|
|
name: String? = nil,
|
|
limit: Int = 20,
|
|
sortBy: String = "downloads"
|
|
) async throws -> [ModelSearchResult] {
|
|
let filters = ModelFilters(
|
|
search: name,
|
|
tags: ["gguf"],
|
|
sort: sortBy,
|
|
direction: -1,
|
|
limit: limit
|
|
)
|
|
return try await searchModels(filters: filters)
|
|
}
|
|
|
|
/// Search Safetensors models
|
|
public func searchSafetensors(
|
|
name: String? = nil,
|
|
limit: Int = 20,
|
|
sortBy: String = "downloads"
|
|
) async throws -> [ModelSearchResult] {
|
|
let filters = ModelFilters(
|
|
search: name,
|
|
tags: ["safetensors"],
|
|
sort: sortBy,
|
|
direction: -1,
|
|
limit: limit
|
|
)
|
|
return try await searchModels(filters: filters)
|
|
}
|
|
|
|
/// Get model details
|
|
public func getModelDetails(repoId: String) async throws -> ModelSearchResult {
|
|
let url = URL(string: "https://huggingface.co/api/models/\(repoId)")!
|
|
let (data, _) = try await session.data(from: url)
|
|
return try JSONDecoder().decode(ModelSearchResult.self, from: data)
|
|
}
|
|
|
|
/// List model files
|
|
public func listModelFiles(repoId: String, revision: String = "main") async throws -> [String] {
|
|
let url = URL(string: "https://huggingface.co/api/models/\(repoId)/tree/\(revision)")!
|
|
let (data, _) = try await session.data(from: url)
|
|
|
|
struct FileInfo: Codable {
|
|
let path: String
|
|
let type: String
|
|
}
|
|
|
|
let files = try JSONDecoder().decode([FileInfo].self, from: data)
|
|
return files.filter { $0.type == "file" }.map { $0.path }
|
|
}
|
|
}
|
|
|
|
/// Model finder errors
|
|
public enum ModelFinderError: Error, LocalizedError {
|
|
case invalidURL
|
|
case requestFailed(String)
|
|
case decodeFailed(String)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .invalidURL:
|
|
return "Invalid URL"
|
|
case .requestFailed(let detail):
|
|
return "Request failed: \(detail)"
|
|
case .decodeFailed(let detail):
|
|
return "Failed to decode response: \(detail)"
|
|
}
|
|
}
|
|
}
|