@dvai-bridge/ios 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +104 -104
- package/ios/Sources/DVAIBridge/BackendKind.swift +23 -23
- package/ios/Sources/DVAIBridge/BoundServer.swift +46 -46
- package/ios/Sources/DVAIBridge/DVAIBridge.swift +658 -658
- package/ios/Sources/DVAIBridge/DVAIBridgeConfig.swift +86 -86
- package/ios/Sources/DVAIBridge/DVAIBridgeError.swift +33 -33
- package/ios/Sources/DVAIBridge/Internal/BackendSelector.swift +59 -59
- package/ios/Sources/DVAIBridge/Internal/ProgressBroadcaster.swift +84 -84
- package/ios/Sources/DVAIBridge/License/Audience.swift +133 -133
- package/ios/Sources/DVAIBridge/License/Discovery.swift +164 -164
- package/ios/Sources/DVAIBridge/License/LicenseValidator.swift +392 -392
- package/ios/Sources/DVAIBridge/License/PublicKeys.swift +114 -114
- package/ios/Sources/DVAIBridge/License/Types.swift +195 -195
- package/ios/Sources/DVAIBridge/Offload/OffloadConfig.swift +118 -118
- package/ios/Sources/DVAIBridge/ProgressEvent.swift +34 -34
- package/ios/Sources/DVAICoreMLCore/CoreMLBackendError.swift +19 -19
- package/ios/Sources/DVAICoreMLCore/CoreMLHandlers.swift +123 -123
- package/ios/Sources/DVAICoreMLCore/CoreMLPluginState.swift +130 -130
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLEngine.swift +137 -137
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLGenerator.swift +108 -108
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLSampler.swift +96 -96
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLTokenizer.swift +69 -69
- package/ios/Tests/DVAIBridgeTests/BackendSelectorTests.swift +53 -53
- package/ios/Tests/DVAIBridgeTests/CoreMLEngineTests.swift +18 -18
- package/ios/Tests/DVAIBridgeTests/CoreMLGeneratorShapeTests.swift +11 -11
- package/ios/Tests/DVAIBridgeTests/CoreMLHandlersTests.swift +32 -32
- package/ios/Tests/DVAIBridgeTests/CoreMLPluginStateTests.swift +41 -41
- package/ios/Tests/DVAIBridgeTests/CoreMLSamplerTests.swift +40 -40
- package/ios/Tests/DVAIBridgeTests/CoreMLTokenizerTests.swift +19 -19
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeAPIShapeTests.swift +37 -37
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeConfigTests.swift +52 -52
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeErrorTests.swift +33 -33
- package/ios/Tests/DVAIBridgeTests/LicenseValidatorTests.swift +658 -658
- package/ios/Tests/DVAIBridgeTests/ProgressBroadcasterTests.swift +69 -69
- package/ios/Tests/DVAIBridgeTests/ProgressEventTests.swift +25 -25
- package/ios/Tests/DVAIBridgeTests/ReactiveStateTests.swift +45 -45
- package/ios/Tests/DVAIBridgeTests/RealModelIntegrationTest.swift +385 -359
- package/package.json +3 -4
- package/DVAIBridge.podspec +0 -120
- package/LICENSE +0 -51
- package/README.md +0 -199
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
public struct DVAIBridgeConfig: Sendable {
|
|
4
|
-
public enum CORSOrigin: Sendable {
|
|
5
|
-
case wildcard
|
|
6
|
-
case exact(String)
|
|
7
|
-
case allowlist([String])
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
public var backend: BackendKind
|
|
11
|
-
public var modelPath: String?
|
|
12
|
-
public var mmprojPath: String?
|
|
13
|
-
public var tokenizerPath: String?
|
|
14
|
-
public var gpuLayers: Int
|
|
15
|
-
public var contextSize: Int
|
|
16
|
-
public var threads: Int
|
|
17
|
-
public var embeddingMode: Bool
|
|
18
|
-
public var httpBasePort: Int
|
|
19
|
-
public var httpMaxPortAttempts: Int
|
|
20
|
-
public var corsOrigin: CORSOrigin
|
|
21
|
-
public var autoUnloadOnLowMemory: Bool
|
|
22
|
-
public var logLevel: String // "silent" | "info" | "debug" — matches the Capacitor surface
|
|
23
|
-
|
|
24
|
-
public init(
|
|
25
|
-
backend: BackendKind = .auto,
|
|
26
|
-
modelPath: String? = nil,
|
|
27
|
-
mmprojPath: String? = nil,
|
|
28
|
-
tokenizerPath: String? = nil,
|
|
29
|
-
gpuLayers: Int = 99,
|
|
30
|
-
contextSize: Int = 2048,
|
|
31
|
-
threads: Int = 4,
|
|
32
|
-
embeddingMode: Bool = false,
|
|
33
|
-
httpBasePort: Int = 38883,
|
|
34
|
-
httpMaxPortAttempts: Int = 16,
|
|
35
|
-
corsOrigin: CORSOrigin = .wildcard,
|
|
36
|
-
autoUnloadOnLowMemory: Bool = false,
|
|
37
|
-
logLevel: String = "info"
|
|
38
|
-
) {
|
|
39
|
-
self.backend = backend
|
|
40
|
-
self.modelPath = modelPath
|
|
41
|
-
self.mmprojPath = mmprojPath
|
|
42
|
-
self.tokenizerPath = tokenizerPath
|
|
43
|
-
self.gpuLayers = gpuLayers
|
|
44
|
-
self.contextSize = contextSize
|
|
45
|
-
self.threads = threads
|
|
46
|
-
self.embeddingMode = embeddingMode
|
|
47
|
-
self.httpBasePort = httpBasePort
|
|
48
|
-
self.httpMaxPortAttempts = httpMaxPortAttempts
|
|
49
|
-
self.corsOrigin = corsOrigin
|
|
50
|
-
self.autoUnloadOnLowMemory = autoUnloadOnLowMemory
|
|
51
|
-
self.logLevel = logLevel
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/// v3.2 — copy with `httpBasePort` overridden. Used by the
|
|
55
|
-
/// OffloadProxy lifecycle to push the backend off the user-facing
|
|
56
|
-
/// port (proxy claims `httpBasePort`, backend gets `httpBasePort + 100`).
|
|
57
|
-
public func with(httpBasePort newPort: Int) -> DVAIBridgeConfig {
|
|
58
|
-
var copy = self
|
|
59
|
-
copy.httpBasePort = newPort
|
|
60
|
-
return copy
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/// Translate this config into the `[String: Any]` shape the underlying
|
|
64
|
-
/// core PluginStates expect (matches the Capacitor JSObject shape).
|
|
65
|
-
internal func toCoreOpts() -> [String: Any] {
|
|
66
|
-
var opts: [String: Any] = [
|
|
67
|
-
"gpuLayers": gpuLayers,
|
|
68
|
-
"contextSize": contextSize,
|
|
69
|
-
"threads": threads,
|
|
70
|
-
"embeddingMode": embeddingMode,
|
|
71
|
-
"httpBasePort": httpBasePort,
|
|
72
|
-
"httpMaxPortAttempts": httpMaxPortAttempts,
|
|
73
|
-
"autoUnloadOnLowMemory": autoUnloadOnLowMemory,
|
|
74
|
-
"logLevel": logLevel,
|
|
75
|
-
]
|
|
76
|
-
if let modelPath { opts["modelPath"] = modelPath }
|
|
77
|
-
if let mmprojPath { opts["mmprojPath"] = mmprojPath }
|
|
78
|
-
if let tokenizerPath { opts["tokenizerPath"] = tokenizerPath }
|
|
79
|
-
switch corsOrigin {
|
|
80
|
-
case .wildcard: opts["corsOrigin"] = "*"
|
|
81
|
-
case .exact(let s): opts["corsOrigin"] = s
|
|
82
|
-
case .allowlist(let xs): opts["corsOrigin"] = xs
|
|
83
|
-
}
|
|
84
|
-
return opts
|
|
85
|
-
}
|
|
86
|
-
}
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public struct DVAIBridgeConfig: Sendable {
|
|
4
|
+
public enum CORSOrigin: Sendable {
|
|
5
|
+
case wildcard
|
|
6
|
+
case exact(String)
|
|
7
|
+
case allowlist([String])
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public var backend: BackendKind
|
|
11
|
+
public var modelPath: String?
|
|
12
|
+
public var mmprojPath: String?
|
|
13
|
+
public var tokenizerPath: String?
|
|
14
|
+
public var gpuLayers: Int
|
|
15
|
+
public var contextSize: Int
|
|
16
|
+
public var threads: Int
|
|
17
|
+
public var embeddingMode: Bool
|
|
18
|
+
public var httpBasePort: Int
|
|
19
|
+
public var httpMaxPortAttempts: Int
|
|
20
|
+
public var corsOrigin: CORSOrigin
|
|
21
|
+
public var autoUnloadOnLowMemory: Bool
|
|
22
|
+
public var logLevel: String // "silent" | "info" | "debug" — matches the Capacitor surface
|
|
23
|
+
|
|
24
|
+
public init(
|
|
25
|
+
backend: BackendKind = .auto,
|
|
26
|
+
modelPath: String? = nil,
|
|
27
|
+
mmprojPath: String? = nil,
|
|
28
|
+
tokenizerPath: String? = nil,
|
|
29
|
+
gpuLayers: Int = 99,
|
|
30
|
+
contextSize: Int = 2048,
|
|
31
|
+
threads: Int = 4,
|
|
32
|
+
embeddingMode: Bool = false,
|
|
33
|
+
httpBasePort: Int = 38883,
|
|
34
|
+
httpMaxPortAttempts: Int = 16,
|
|
35
|
+
corsOrigin: CORSOrigin = .wildcard,
|
|
36
|
+
autoUnloadOnLowMemory: Bool = false,
|
|
37
|
+
logLevel: String = "info"
|
|
38
|
+
) {
|
|
39
|
+
self.backend = backend
|
|
40
|
+
self.modelPath = modelPath
|
|
41
|
+
self.mmprojPath = mmprojPath
|
|
42
|
+
self.tokenizerPath = tokenizerPath
|
|
43
|
+
self.gpuLayers = gpuLayers
|
|
44
|
+
self.contextSize = contextSize
|
|
45
|
+
self.threads = threads
|
|
46
|
+
self.embeddingMode = embeddingMode
|
|
47
|
+
self.httpBasePort = httpBasePort
|
|
48
|
+
self.httpMaxPortAttempts = httpMaxPortAttempts
|
|
49
|
+
self.corsOrigin = corsOrigin
|
|
50
|
+
self.autoUnloadOnLowMemory = autoUnloadOnLowMemory
|
|
51
|
+
self.logLevel = logLevel
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// v3.2 — copy with `httpBasePort` overridden. Used by the
|
|
55
|
+
/// OffloadProxy lifecycle to push the backend off the user-facing
|
|
56
|
+
/// port (proxy claims `httpBasePort`, backend gets `httpBasePort + 100`).
|
|
57
|
+
public func with(httpBasePort newPort: Int) -> DVAIBridgeConfig {
|
|
58
|
+
var copy = self
|
|
59
|
+
copy.httpBasePort = newPort
|
|
60
|
+
return copy
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Translate this config into the `[String: Any]` shape the underlying
|
|
64
|
+
/// core PluginStates expect (matches the Capacitor JSObject shape).
|
|
65
|
+
internal func toCoreOpts() -> [String: Any] {
|
|
66
|
+
var opts: [String: Any] = [
|
|
67
|
+
"gpuLayers": gpuLayers,
|
|
68
|
+
"contextSize": contextSize,
|
|
69
|
+
"threads": threads,
|
|
70
|
+
"embeddingMode": embeddingMode,
|
|
71
|
+
"httpBasePort": httpBasePort,
|
|
72
|
+
"httpMaxPortAttempts": httpMaxPortAttempts,
|
|
73
|
+
"autoUnloadOnLowMemory": autoUnloadOnLowMemory,
|
|
74
|
+
"logLevel": logLevel,
|
|
75
|
+
]
|
|
76
|
+
if let modelPath { opts["modelPath"] = modelPath }
|
|
77
|
+
if let mmprojPath { opts["mmprojPath"] = mmprojPath }
|
|
78
|
+
if let tokenizerPath { opts["tokenizerPath"] = tokenizerPath }
|
|
79
|
+
switch corsOrigin {
|
|
80
|
+
case .wildcard: opts["corsOrigin"] = "*"
|
|
81
|
+
case .exact(let s): opts["corsOrigin"] = s
|
|
82
|
+
case .allowlist(let xs): opts["corsOrigin"] = xs
|
|
83
|
+
}
|
|
84
|
+
return opts
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
public enum DVAIBridgeError: Error, LocalizedError, Sendable {
|
|
4
|
-
case notStarted
|
|
5
|
-
case alreadyStarted(currentBackend: BackendKind, baseUrl: String)
|
|
6
|
-
case configurationInvalid(reason: String)
|
|
7
|
-
case backendUnavailable(BackendKind, reason: String)
|
|
8
|
-
case modelLoadFailed(reason: String)
|
|
9
|
-
case downloadFailed(reason: String)
|
|
10
|
-
case checksumMismatch
|
|
11
|
-
case backendError(underlying: String)
|
|
12
|
-
|
|
13
|
-
public var errorDescription: String? {
|
|
14
|
-
switch self {
|
|
15
|
-
case .notStarted:
|
|
16
|
-
return "DVAIBridge has not been started. Call DVAIBridge.shared.start(...) first."
|
|
17
|
-
case .alreadyStarted(let backend, let baseUrl):
|
|
18
|
-
return "DVAIBridge is already running with backend \(backend) at \(baseUrl). Call stop() before starting a new session."
|
|
19
|
-
case .configurationInvalid(let reason):
|
|
20
|
-
return "Configuration invalid: \(reason)"
|
|
21
|
-
case .backendUnavailable(let backend, let reason):
|
|
22
|
-
return "Backend \(backend) is unavailable on this device: \(reason)"
|
|
23
|
-
case .modelLoadFailed(let reason):
|
|
24
|
-
return "Model load failed: \(reason)"
|
|
25
|
-
case .downloadFailed(let reason):
|
|
26
|
-
return "Download failed: \(reason)"
|
|
27
|
-
case .checksumMismatch:
|
|
28
|
-
return "Downloaded file's SHA-256 didn't match the expected value. The file has been deleted from the cache."
|
|
29
|
-
case .backendError(let msg):
|
|
30
|
-
return "Backend error: \(msg)"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public enum DVAIBridgeError: Error, LocalizedError, Sendable {
|
|
4
|
+
case notStarted
|
|
5
|
+
case alreadyStarted(currentBackend: BackendKind, baseUrl: String)
|
|
6
|
+
case configurationInvalid(reason: String)
|
|
7
|
+
case backendUnavailable(BackendKind, reason: String)
|
|
8
|
+
case modelLoadFailed(reason: String)
|
|
9
|
+
case downloadFailed(reason: String)
|
|
10
|
+
case checksumMismatch
|
|
11
|
+
case backendError(underlying: String)
|
|
12
|
+
|
|
13
|
+
public var errorDescription: String? {
|
|
14
|
+
switch self {
|
|
15
|
+
case .notStarted:
|
|
16
|
+
return "DVAIBridge has not been started. Call DVAIBridge.shared.start(...) first."
|
|
17
|
+
case .alreadyStarted(let backend, let baseUrl):
|
|
18
|
+
return "DVAIBridge is already running with backend \(backend) at \(baseUrl). Call stop() before starting a new session."
|
|
19
|
+
case .configurationInvalid(let reason):
|
|
20
|
+
return "Configuration invalid: \(reason)"
|
|
21
|
+
case .backendUnavailable(let backend, let reason):
|
|
22
|
+
return "Backend \(backend) is unavailable on this device: \(reason)"
|
|
23
|
+
case .modelLoadFailed(let reason):
|
|
24
|
+
return "Model load failed: \(reason)"
|
|
25
|
+
case .downloadFailed(let reason):
|
|
26
|
+
return "Download failed: \(reason)"
|
|
27
|
+
case .checksumMismatch:
|
|
28
|
+
return "Downloaded file's SHA-256 didn't match the expected value. The file has been deleted from the cache."
|
|
29
|
+
case .backendError(let msg):
|
|
30
|
+
return "Backend error: \(msg)"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
internal enum BackendSelector {
|
|
4
|
-
/// Resolve `.auto` to a concrete backend; pass-through for explicit choices.
|
|
5
|
-
/// - Throws `DVAIBridgeError.configurationInvalid` if `.auto` can't decide.
|
|
6
|
-
static func resolve(_ kind: BackendKind, config: DVAIBridgeConfig) throws -> BackendKind {
|
|
7
|
-
if kind != .auto { return kind }
|
|
8
|
-
|
|
9
|
-
// 1. modelPath ending in .gguf → .llama
|
|
10
|
-
if let path = config.modelPath, path.hasSuffix(".gguf") {
|
|
11
|
-
return .llama
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// 2. modelPath ending in .mlmodelc / .mlpackage → .coreml
|
|
15
|
-
if let path = config.modelPath,
|
|
16
|
-
path.hasSuffix(".mlmodelc") || path.hasSuffix(".mlpackage") {
|
|
17
|
-
return .coreml
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// 3. modelPath ending in .task / .litertlm → no iOS backend supports
|
|
21
|
-
// those; fall through to error
|
|
22
|
-
if let path = config.modelPath,
|
|
23
|
-
path.hasSuffix(".task") || path.hasSuffix(".litertlm") {
|
|
24
|
-
throw DVAIBridgeError.configurationInvalid(reason:
|
|
25
|
-
"Model file '\(path)' is a MediaPipe / LiteRT-LM format. " +
|
|
26
|
-
"Use it via the Android SDK; iOS supports llama.cpp (.gguf), " +
|
|
27
|
-
"Apple Foundation Models (no file), and CoreML (.mlmodelc / .mlpackage).")
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// 4. No modelPath + iOS 26+ → .foundation
|
|
31
|
-
if config.modelPath == nil {
|
|
32
|
-
if #available(iOS 26.0, macOS 26.0, *) {
|
|
33
|
-
return .foundation
|
|
34
|
-
}
|
|
35
|
-
throw DVAIBridgeError.configurationInvalid(reason:
|
|
36
|
-
"auto backend requires either modelPath (for .llama / .coreml) " +
|
|
37
|
-
"or iOS 26+ (for .foundation). Set DVAIBridgeConfig.backend explicitly.")
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 5. modelPath looks like a HuggingFace id ("<owner>/<repo>" with no
|
|
41
|
-
// file extension) → likely MLX. Don't auto-resolve here because
|
|
42
|
-
// not every HF id is MLX (could be GGUF in a HF repo etc.) and
|
|
43
|
-
// .mlx requires Apple Silicon at runtime. Provide a clear hint.
|
|
44
|
-
if let path = config.modelPath,
|
|
45
|
-
path.contains("/"),
|
|
46
|
-
!path.contains(".") {
|
|
47
|
-
throw DVAIBridgeError.configurationInvalid(reason:
|
|
48
|
-
"modelPath '\(path)' looks like a HuggingFace identifier. " +
|
|
49
|
-
"If this is an MLX-converted checkpoint (e.g. 'mlx-community/...'), " +
|
|
50
|
-
"set DVAIBridgeConfig.backend = .mlx explicitly — `.auto` won't " +
|
|
51
|
-
"infer MLX because not every HF id is an MLX checkpoint.")
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 6. Unknown extension
|
|
55
|
-
throw DVAIBridgeError.configurationInvalid(reason:
|
|
56
|
-
"auto backend can't infer from modelPath '\(config.modelPath ?? "<nil>")'. " +
|
|
57
|
-
"Set DVAIBridgeConfig.backend = .llama / .foundation / .coreml / .mlx explicitly.")
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
internal enum BackendSelector {
|
|
4
|
+
/// Resolve `.auto` to a concrete backend; pass-through for explicit choices.
|
|
5
|
+
/// - Throws `DVAIBridgeError.configurationInvalid` if `.auto` can't decide.
|
|
6
|
+
static func resolve(_ kind: BackendKind, config: DVAIBridgeConfig) throws -> BackendKind {
|
|
7
|
+
if kind != .auto { return kind }
|
|
8
|
+
|
|
9
|
+
// 1. modelPath ending in .gguf → .llama
|
|
10
|
+
if let path = config.modelPath, path.hasSuffix(".gguf") {
|
|
11
|
+
return .llama
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 2. modelPath ending in .mlmodelc / .mlpackage → .coreml
|
|
15
|
+
if let path = config.modelPath,
|
|
16
|
+
path.hasSuffix(".mlmodelc") || path.hasSuffix(".mlpackage") {
|
|
17
|
+
return .coreml
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 3. modelPath ending in .task / .litertlm → no iOS backend supports
|
|
21
|
+
// those; fall through to error
|
|
22
|
+
if let path = config.modelPath,
|
|
23
|
+
path.hasSuffix(".task") || path.hasSuffix(".litertlm") {
|
|
24
|
+
throw DVAIBridgeError.configurationInvalid(reason:
|
|
25
|
+
"Model file '\(path)' is a MediaPipe / LiteRT-LM format. " +
|
|
26
|
+
"Use it via the Android SDK; iOS supports llama.cpp (.gguf), " +
|
|
27
|
+
"Apple Foundation Models (no file), and CoreML (.mlmodelc / .mlpackage).")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 4. No modelPath + iOS 26+ → .foundation
|
|
31
|
+
if config.modelPath == nil {
|
|
32
|
+
if #available(iOS 26.0, macOS 26.0, *) {
|
|
33
|
+
return .foundation
|
|
34
|
+
}
|
|
35
|
+
throw DVAIBridgeError.configurationInvalid(reason:
|
|
36
|
+
"auto backend requires either modelPath (for .llama / .coreml) " +
|
|
37
|
+
"or iOS 26+ (for .foundation). Set DVAIBridgeConfig.backend explicitly.")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 5. modelPath looks like a HuggingFace id ("<owner>/<repo>" with no
|
|
41
|
+
// file extension) → likely MLX. Don't auto-resolve here because
|
|
42
|
+
// not every HF id is MLX (could be GGUF in a HF repo etc.) and
|
|
43
|
+
// .mlx requires Apple Silicon at runtime. Provide a clear hint.
|
|
44
|
+
if let path = config.modelPath,
|
|
45
|
+
path.contains("/"),
|
|
46
|
+
!path.contains(".") {
|
|
47
|
+
throw DVAIBridgeError.configurationInvalid(reason:
|
|
48
|
+
"modelPath '\(path)' looks like a HuggingFace identifier. " +
|
|
49
|
+
"If this is an MLX-converted checkpoint (e.g. 'mlx-community/...'), " +
|
|
50
|
+
"set DVAIBridgeConfig.backend = .mlx explicitly — `.auto` won't " +
|
|
51
|
+
"infer MLX because not every HF id is an MLX checkpoint.")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 6. Unknown extension
|
|
55
|
+
throw DVAIBridgeError.configurationInvalid(reason:
|
|
56
|
+
"auto backend can't infer from modelPath '\(config.modelPath ?? "<nil>")'. " +
|
|
57
|
+
"Set DVAIBridgeConfig.backend = .llama / .foundation / .coreml / .mlx explicitly.")
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import Combine
|
|
3
|
-
|
|
4
|
-
/// Internal event broadcaster. Backs three public observation surfaces:
|
|
5
|
-
/// `progressPublisher` (Combine), `progressStream` (AsyncStream), and
|
|
6
|
-
/// `addProgressListener(_:)` (callback). All three observe the same source.
|
|
7
|
-
internal final class ProgressBroadcaster: @unchecked Sendable {
|
|
8
|
-
// Combine
|
|
9
|
-
private let subject = PassthroughSubject<ProgressEvent, Never>()
|
|
10
|
-
var publisher: AnyPublisher<ProgressEvent, Never> { subject.eraseToAnyPublisher() }
|
|
11
|
-
|
|
12
|
-
// AsyncStream — one continuation per consumer
|
|
13
|
-
private let lock = NSLock()
|
|
14
|
-
private var continuations: [UUID: AsyncStream<ProgressEvent>.Continuation] = [:]
|
|
15
|
-
|
|
16
|
-
// Callback — one entry per addProgressListener call
|
|
17
|
-
private var callbacks: [UUID: @Sendable (ProgressEvent) -> Void] = [:]
|
|
18
|
-
|
|
19
|
-
func emit(_ event: ProgressEvent) {
|
|
20
|
-
subject.send(event)
|
|
21
|
-
|
|
22
|
-
lock.lock()
|
|
23
|
-
let conts = continuations.values
|
|
24
|
-
let cbs = Array(callbacks.values)
|
|
25
|
-
lock.unlock()
|
|
26
|
-
|
|
27
|
-
for cont in conts { cont.yield(event) }
|
|
28
|
-
for cb in cbs { cb(event) }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
func makeStream() -> AsyncStream<ProgressEvent> {
|
|
32
|
-
let id = UUID()
|
|
33
|
-
return AsyncStream { continuation in
|
|
34
|
-
lock.lock()
|
|
35
|
-
continuations[id] = continuation
|
|
36
|
-
lock.unlock()
|
|
37
|
-
|
|
38
|
-
continuation.onTermination = { [weak self] _ in
|
|
39
|
-
self?.lock.lock()
|
|
40
|
-
self?.continuations.removeValue(forKey: id)
|
|
41
|
-
self?.lock.unlock()
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@discardableResult
|
|
47
|
-
func addCallback(_ cb: @escaping @Sendable (ProgressEvent) -> Void) -> CancellationToken {
|
|
48
|
-
let id = UUID()
|
|
49
|
-
lock.lock()
|
|
50
|
-
callbacks[id] = cb
|
|
51
|
-
lock.unlock()
|
|
52
|
-
|
|
53
|
-
return CancellationToken { [weak self] in
|
|
54
|
-
self?.lock.lock()
|
|
55
|
-
self?.callbacks.removeValue(forKey: id)
|
|
56
|
-
self?.lock.unlock()
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/// Caller-held token returned by `addProgressListener(_:)`. Drop or call
|
|
62
|
-
/// `.cancel()` to stop receiving events.
|
|
63
|
-
public final class CancellationToken: @unchecked Sendable {
|
|
64
|
-
private let cancelClosure: @Sendable () -> Void
|
|
65
|
-
private var cancelled = false
|
|
66
|
-
private let lock = NSLock()
|
|
67
|
-
|
|
68
|
-
internal init(cancel: @escaping @Sendable () -> Void) {
|
|
69
|
-
self.cancelClosure = cancel
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
public func cancel() {
|
|
73
|
-
lock.lock()
|
|
74
|
-
defer { lock.unlock() }
|
|
75
|
-
if !cancelled {
|
|
76
|
-
cancelled = true
|
|
77
|
-
cancelClosure()
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
deinit {
|
|
82
|
-
cancel()
|
|
83
|
-
}
|
|
84
|
-
}
|
|
1
|
+
import Foundation
|
|
2
|
+
import Combine
|
|
3
|
+
|
|
4
|
+
/// Internal event broadcaster. Backs three public observation surfaces:
|
|
5
|
+
/// `progressPublisher` (Combine), `progressStream` (AsyncStream), and
|
|
6
|
+
/// `addProgressListener(_:)` (callback). All three observe the same source.
|
|
7
|
+
internal final class ProgressBroadcaster: @unchecked Sendable {
|
|
8
|
+
// Combine
|
|
9
|
+
private let subject = PassthroughSubject<ProgressEvent, Never>()
|
|
10
|
+
var publisher: AnyPublisher<ProgressEvent, Never> { subject.eraseToAnyPublisher() }
|
|
11
|
+
|
|
12
|
+
// AsyncStream — one continuation per consumer
|
|
13
|
+
private let lock = NSLock()
|
|
14
|
+
private var continuations: [UUID: AsyncStream<ProgressEvent>.Continuation] = [:]
|
|
15
|
+
|
|
16
|
+
// Callback — one entry per addProgressListener call
|
|
17
|
+
private var callbacks: [UUID: @Sendable (ProgressEvent) -> Void] = [:]
|
|
18
|
+
|
|
19
|
+
func emit(_ event: ProgressEvent) {
|
|
20
|
+
subject.send(event)
|
|
21
|
+
|
|
22
|
+
lock.lock()
|
|
23
|
+
let conts = continuations.values
|
|
24
|
+
let cbs = Array(callbacks.values)
|
|
25
|
+
lock.unlock()
|
|
26
|
+
|
|
27
|
+
for cont in conts { cont.yield(event) }
|
|
28
|
+
for cb in cbs { cb(event) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func makeStream() -> AsyncStream<ProgressEvent> {
|
|
32
|
+
let id = UUID()
|
|
33
|
+
return AsyncStream { continuation in
|
|
34
|
+
lock.lock()
|
|
35
|
+
continuations[id] = continuation
|
|
36
|
+
lock.unlock()
|
|
37
|
+
|
|
38
|
+
continuation.onTermination = { [weak self] _ in
|
|
39
|
+
self?.lock.lock()
|
|
40
|
+
self?.continuations.removeValue(forKey: id)
|
|
41
|
+
self?.lock.unlock()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@discardableResult
|
|
47
|
+
func addCallback(_ cb: @escaping @Sendable (ProgressEvent) -> Void) -> CancellationToken {
|
|
48
|
+
let id = UUID()
|
|
49
|
+
lock.lock()
|
|
50
|
+
callbacks[id] = cb
|
|
51
|
+
lock.unlock()
|
|
52
|
+
|
|
53
|
+
return CancellationToken { [weak self] in
|
|
54
|
+
self?.lock.lock()
|
|
55
|
+
self?.callbacks.removeValue(forKey: id)
|
|
56
|
+
self?.lock.unlock()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Caller-held token returned by `addProgressListener(_:)`. Drop or call
|
|
62
|
+
/// `.cancel()` to stop receiving events.
|
|
63
|
+
public final class CancellationToken: @unchecked Sendable {
|
|
64
|
+
private let cancelClosure: @Sendable () -> Void
|
|
65
|
+
private var cancelled = false
|
|
66
|
+
private let lock = NSLock()
|
|
67
|
+
|
|
68
|
+
internal init(cancel: @escaping @Sendable () -> Void) {
|
|
69
|
+
self.cancelClosure = cancel
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public func cancel() {
|
|
73
|
+
lock.lock()
|
|
74
|
+
defer { lock.unlock() }
|
|
75
|
+
if !cancelled {
|
|
76
|
+
cancelled = true
|
|
77
|
+
cancelClosure()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
deinit {
|
|
82
|
+
cancel()
|
|
83
|
+
}
|
|
84
|
+
}
|