v2: Initial clean branch with unit tests + CI/CD pipeline
- Started from ac75faa (initial E4B-MarkBase integration)
- Kept Sources/ (all engine code) + Package.swift + .gitignore
- Removed all ad-hoc tests, documentation, scripts, Python files
- Added Tests/00_Unit/ (MathTest, TokenizerTest, SamplerTest)
- Added .gitea/workflows/ci.yaml (build + unit tests + lint)
- Added Scripts/check_resources.sh (memory-aware test runner)
- Added Tests/Manifest.json (resource requirements for all tests)
- Focus: 4-bit quantized models only
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user