@dvai-bridge/ios 4.0.0

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.
Files changed (60) hide show
  1. package/DVAIBridge.podspec +120 -0
  2. package/LICENSE +51 -0
  3. package/Package.swift +104 -0
  4. package/README.md +199 -0
  5. package/ios/Sources/DVAIBridge/BackendKind.swift +23 -0
  6. package/ios/Sources/DVAIBridge/BoundServer.swift +46 -0
  7. package/ios/Sources/DVAIBridge/Capability/CapabilityCache.swift +85 -0
  8. package/ios/Sources/DVAIBridge/Capability/CapabilityPrecheck.swift +193 -0
  9. package/ios/Sources/DVAIBridge/Capability/CapabilityScore.swift +51 -0
  10. package/ios/Sources/DVAIBridge/Capability/DeviceID.swift +70 -0
  11. package/ios/Sources/DVAIBridge/Capability/HardwareAssessment.swift +41 -0
  12. package/ios/Sources/DVAIBridge/DVAIBridge.swift +658 -0
  13. package/ios/Sources/DVAIBridge/DVAIBridgeConfig.swift +86 -0
  14. package/ios/Sources/DVAIBridge/DVAIBridgeError.swift +33 -0
  15. package/ios/Sources/DVAIBridge/Discovery/MDNSPeer.swift +64 -0
  16. package/ios/Sources/DVAIBridge/Discovery/NWAdvertiser.swift +103 -0
  17. package/ios/Sources/DVAIBridge/Discovery/NWBrowserDiscovery.swift +212 -0
  18. package/ios/Sources/DVAIBridge/Internal/BackendSelector.swift +59 -0
  19. package/ios/Sources/DVAIBridge/Internal/ProgressBroadcaster.swift +84 -0
  20. package/ios/Sources/DVAIBridge/License/Audience.swift +133 -0
  21. package/ios/Sources/DVAIBridge/License/Discovery.swift +164 -0
  22. package/ios/Sources/DVAIBridge/License/LicenseValidator.swift +392 -0
  23. package/ios/Sources/DVAIBridge/License/PublicKeys.swift +114 -0
  24. package/ios/Sources/DVAIBridge/License/Types.swift +195 -0
  25. package/ios/Sources/DVAIBridge/Offload/OffloadConfig.swift +118 -0
  26. package/ios/Sources/DVAIBridge/Offload/OffloadProxy.swift +604 -0
  27. package/ios/Sources/DVAIBridge/Offload/OffloadRuntime.swift +98 -0
  28. package/ios/Sources/DVAIBridge/Pairing/Pairing.swift +125 -0
  29. package/ios/Sources/DVAIBridge/Pairing/PairingHandshake.swift +141 -0
  30. package/ios/Sources/DVAIBridge/Pairing/PairingPolicy.swift +162 -0
  31. package/ios/Sources/DVAIBridge/Pairing/PairingStore.swift +65 -0
  32. package/ios/Sources/DVAIBridge/ProgressEvent.swift +34 -0
  33. package/ios/Sources/DVAIBridge/ReactiveState.swift +149 -0
  34. package/ios/Sources/DVAICoreMLCore/.gitkeep +0 -0
  35. package/ios/Sources/DVAICoreMLCore/CoreMLBackendError.swift +19 -0
  36. package/ios/Sources/DVAICoreMLCore/CoreMLHandlers.swift +123 -0
  37. package/ios/Sources/DVAICoreMLCore/CoreMLPluginState.swift +130 -0
  38. package/ios/Sources/DVAICoreMLCore/Internal/CoreMLEngine.swift +137 -0
  39. package/ios/Sources/DVAICoreMLCore/Internal/CoreMLGenerator.swift +108 -0
  40. package/ios/Sources/DVAICoreMLCore/Internal/CoreMLSampler.swift +96 -0
  41. package/ios/Sources/DVAICoreMLCore/Internal/CoreMLTokenizer.swift +69 -0
  42. package/ios/Tests/DVAIBridgeTests/BackendSelectorTests.swift +53 -0
  43. package/ios/Tests/DVAIBridgeTests/CapabilityPrecheckTests.swift +108 -0
  44. package/ios/Tests/DVAIBridgeTests/CoreMLEngineTests.swift +18 -0
  45. package/ios/Tests/DVAIBridgeTests/CoreMLGeneratorShapeTests.swift +11 -0
  46. package/ios/Tests/DVAIBridgeTests/CoreMLHandlersTests.swift +32 -0
  47. package/ios/Tests/DVAIBridgeTests/CoreMLPluginStateTests.swift +41 -0
  48. package/ios/Tests/DVAIBridgeTests/CoreMLSamplerTests.swift +40 -0
  49. package/ios/Tests/DVAIBridgeTests/CoreMLTokenizerTests.swift +19 -0
  50. package/ios/Tests/DVAIBridgeTests/DVAIBridgeAPIShapeTests.swift +37 -0
  51. package/ios/Tests/DVAIBridgeTests/DVAIBridgeConfigTests.swift +52 -0
  52. package/ios/Tests/DVAIBridgeTests/DVAIBridgeErrorTests.swift +33 -0
  53. package/ios/Tests/DVAIBridgeTests/LicenseValidatorTests.swift +658 -0
  54. package/ios/Tests/DVAIBridgeTests/OffloadProxyDecisionTests.swift +156 -0
  55. package/ios/Tests/DVAIBridgeTests/OffloadTests.swift +339 -0
  56. package/ios/Tests/DVAIBridgeTests/ProgressBroadcasterTests.swift +69 -0
  57. package/ios/Tests/DVAIBridgeTests/ProgressEventTests.swift +25 -0
  58. package/ios/Tests/DVAIBridgeTests/ReactiveStateTests.swift +45 -0
  59. package/ios/Tests/DVAIBridgeTests/RealModelIntegrationTest.swift +359 -0
  60. package/package.json +19 -0
@@ -0,0 +1,193 @@
1
+ import Foundation
2
+ #if canImport(UIKit)
3
+ import UIKit
4
+ #endif
5
+
6
+ /// v3.2 — pre-init capability gate (Swift port of the Kotlin
7
+ /// `CapabilityPrecheck` and the TS `assessCapability`).
8
+ ///
9
+ /// Mirrors the Android + TS heuristic 1:1 — same hint shapes, same
10
+ /// threshold defaults (3 tok/s hardware floor, 10 tok/s comfort
11
+ /// threshold), same three modes. Guarantees that a given device
12
+ /// classifies the same way regardless of which platform's SDK is
13
+ /// asking.
14
+ ///
15
+ /// Heuristic-only: no model is loaded, no probe runs. The cold-run
16
+ /// probe (`probeCapability`) refines the estimate AFTER the model is
17
+ /// loaded and a request has actually completed; that path is unchanged
18
+ /// from v3.0.
19
+ public enum CapabilityPrecheck {
20
+
21
+ /// Result of `assess(...)`. `Codable` so it can serialize cleanly
22
+ /// to JSON for cross-language Pigeon / Capacitor bridges.
23
+ public struct Result: Sendable, Codable, Equatable {
24
+ public let mode: PrecheckMode
25
+ /// Estimated decode tok/s for any 1–3B-class model.
26
+ public let tokPerSec: Double
27
+ public let hints: DeviceCapabilityHints
28
+ /// Human-readable explanation; safe to log + display.
29
+ public let reason: String
30
+ }
31
+
32
+ /// Tunable thresholds. Defaults match `OffloadConfig` / the Kotlin
33
+ /// `CapabilityPrecheck.Thresholds` / the TS defaults.
34
+ public struct Thresholds: Sendable {
35
+ /// Below this, the device is too weak to run inference at all.
36
+ /// Default 3.0 tok/s.
37
+ public var hardwareMinimum: Double
38
+ /// Below this, run in offload-only mode. Default 10.0.
39
+ public var minLocalCapability: Double
40
+
41
+ public init(hardwareMinimum: Double = 3.0, minLocalCapability: Double = 10.0) {
42
+ self.hardwareMinimum = hardwareMinimum
43
+ self.minLocalCapability = minLocalCapability
44
+ }
45
+ }
46
+
47
+ /// Run the precheck. Pass `hints` to override the auto-detect (used
48
+ /// by tests + cross-platform parity checks).
49
+ public static func assess(
50
+ thresholds: Thresholds = Thresholds(),
51
+ hints: DeviceCapabilityHints? = nil
52
+ ) -> Result {
53
+ let resolvedHints = hints ?? detectDeviceHints()
54
+ let tokPerSec = heuristicTokPerSec(hints: resolvedHints)
55
+
56
+ if tokPerSec < thresholds.hardwareMinimum {
57
+ return Result(
58
+ mode: .tooWeak,
59
+ tokPerSec: tokPerSec,
60
+ hints: resolvedHints,
61
+ reason: "estimated \(tokPerSec) tok/s, below the " +
62
+ "\(thresholds.hardwareMinimum) tok/s hardware floor — " +
63
+ "local inference would be unusable."
64
+ )
65
+ }
66
+
67
+ if tokPerSec < thresholds.minLocalCapability {
68
+ return Result(
69
+ mode: .offloadOnly,
70
+ tokPerSec: tokPerSec,
71
+ hints: resolvedHints,
72
+ reason: "estimated \(tokPerSec) tok/s, below the " +
73
+ "\(thresholds.minLocalCapability) tok/s comfort threshold — " +
74
+ "model will not be loaded locally; every request will be " +
75
+ "forwarded to a paired peer."
76
+ )
77
+ }
78
+
79
+ return Result(
80
+ mode: .ok,
81
+ tokPerSec: tokPerSec,
82
+ hints: resolvedHints,
83
+ reason: "estimated \(tokPerSec) tok/s, above the " +
84
+ "\(thresholds.minLocalCapability) tok/s threshold — running normally."
85
+ )
86
+ }
87
+
88
+ /// Pure heuristic — mirrors the TS `heuristicTokPerSec` and
89
+ /// Kotlin's `CapabilityPrecheck.heuristicTokPerSec`.
90
+ public static func heuristicTokPerSec(hints: DeviceCapabilityHints) -> Double {
91
+ // Base score by GPU class — observed floors for 1–3B q4 GGUFs.
92
+ let gpuBase: Double
93
+ switch hints.gpuClass {
94
+ case .none: gpuBase = 3.0
95
+ case .integrated: gpuBase = 8.0
96
+ case .discrete: gpuBase = 35.0
97
+ case .appleSilicon: gpuBase = 40.0
98
+ }
99
+
100
+ let cpuMul: Double
101
+ switch hints.cpuClass {
102
+ case .low: cpuMul = 0.6
103
+ case .mid: cpuMul = 1.0
104
+ case .high: cpuMul = 1.3
105
+ }
106
+
107
+ let ramMul: Double
108
+ if hints.ramGb < 4 { ramMul = 0.3 }
109
+ else if hints.ramGb < 8 { ramMul = 0.7 }
110
+ else { ramMul = 1.0 }
111
+
112
+ let npuBonus: Double = hints.hasNpu ? 1.4 : 1.0
113
+
114
+ let raw = gpuBase * cpuMul * ramMul * npuBonus
115
+ return (raw * 10).rounded() / 10
116
+ }
117
+
118
+ /// Best-effort introspection on iOS / macOS via sysctl + ProcessInfo.
119
+ public static func detectDeviceHints() -> DeviceCapabilityHints {
120
+ let physicalMemoryBytes = ProcessInfo.processInfo.physicalMemory
121
+ let ramGb = Int(Double(physicalMemoryBytes) / (1024.0 * 1024.0 * 1024.0))
122
+ let cores = ProcessInfo.processInfo.activeProcessorCount
123
+
124
+ let cpuClass: CpuClass
125
+ if cores >= 8 { cpuClass = .high }
126
+ else if cores >= 4 { cpuClass = .mid }
127
+ else { cpuClass = .low }
128
+
129
+ // Apple Silicon Macs + iPhones with M-series / A-series chips
130
+ // have unified-memory architectures. We treat them as the
131
+ // `appleSilicon` class — the heuristic uses gpuBase = 40.
132
+ // Older Intel Macs report `appleSilicon` if the runtime is on
133
+ // arm64; we rely on `arch == arm64` as the signal.
134
+ let gpuClass: GpuClass = isAppleSilicon() ? .appleSilicon : .integrated
135
+
136
+ // NPU detection: every Apple Silicon device since A11 has the
137
+ // Neural Engine, and every Intel Mac since 2020 has none. Use
138
+ // arch as a proxy.
139
+ let hasNpu = isAppleSilicon()
140
+
141
+ return DeviceCapabilityHints(
142
+ hasNpu: hasNpu,
143
+ ramGb: ramGb,
144
+ gpuClass: gpuClass,
145
+ cpuClass: cpuClass
146
+ )
147
+ }
148
+
149
+ private static func isAppleSilicon() -> Bool {
150
+ #if arch(arm64)
151
+ return true
152
+ #else
153
+ return false
154
+ #endif
155
+ }
156
+ }
157
+
158
+ /// Lifecycle mode the SDK enters on `start()`. JSON-serialized as a
159
+ /// lower-cased dash string for cross-platform parity (`ok` /
160
+ /// `offload-only` / `too-weak`).
161
+ public enum PrecheckMode: String, Sendable, Codable, Equatable {
162
+ case ok
163
+ case offloadOnly = "offload-only"
164
+ case tooWeak = "too-weak"
165
+ }
166
+
167
+ /// Mirrors the Kotlin `DeviceCapabilityHints` and TS interface.
168
+ public struct DeviceCapabilityHints: Sendable, Codable, Equatable {
169
+ public let hasNpu: Bool
170
+ public let ramGb: Int
171
+ public let gpuClass: GpuClass
172
+ public let cpuClass: CpuClass
173
+
174
+ public init(hasNpu: Bool, ramGb: Int, gpuClass: GpuClass, cpuClass: CpuClass) {
175
+ self.hasNpu = hasNpu
176
+ self.ramGb = ramGb
177
+ self.gpuClass = gpuClass
178
+ self.cpuClass = cpuClass
179
+ }
180
+ }
181
+
182
+ public enum GpuClass: String, Sendable, Codable, Equatable {
183
+ case none
184
+ case integrated
185
+ case discrete
186
+ case appleSilicon = "apple-silicon"
187
+ }
188
+
189
+ public enum CpuClass: String, Sendable, Codable, Equatable {
190
+ case low
191
+ case mid
192
+ case high
193
+ }
@@ -0,0 +1,51 @@
1
+ import Foundation
2
+
3
+ /// Swift-side `CapabilityScore` mirroring the TypeScript shape in
4
+ /// `packages/dvai-bridge-core/src/capability/types.ts`. Persisted by
5
+ /// `CapabilityCache`.
6
+ public struct CapabilityScore: Sendable, Equatable, Codable, Hashable {
7
+ /// Source of the estimate.
8
+ public enum Source: String, Sendable, Codable, Hashable {
9
+ case probe
10
+ case heuristic
11
+ }
12
+
13
+ /// Model identifier this score applies to.
14
+ public let modelId: String
15
+ /// Stable per-install device identifier.
16
+ public let deviceId: String
17
+ /// Library SemVer at the time the score was measured.
18
+ public let libraryVersion: String
19
+ /// Estimated decode rate, tokens-per-second.
20
+ public let tokPerSec: Double
21
+ /// Source of the estimate.
22
+ public let source: Source
23
+ /// Unix milliseconds the score was measured / computed.
24
+ public let measuredAt: Int64
25
+
26
+ public init(
27
+ modelId: String,
28
+ deviceId: String,
29
+ libraryVersion: String,
30
+ tokPerSec: Double,
31
+ source: Source = .heuristic,
32
+ measuredAt: Int64 = Int64(Date().timeIntervalSince1970 * 1000)
33
+ ) {
34
+ self.modelId = modelId
35
+ self.deviceId = deviceId
36
+ self.libraryVersion = libraryVersion
37
+ self.tokPerSec = tokPerSec
38
+ self.source = source
39
+ self.measuredAt = measuredAt
40
+ }
41
+ }
42
+
43
+ public struct CapabilityCacheKey: Sendable, Hashable {
44
+ public let modelId: String
45
+ public let libraryVersion: String
46
+
47
+ public init(modelId: String, libraryVersion: String) {
48
+ self.modelId = modelId
49
+ self.libraryVersion = libraryVersion
50
+ }
51
+ }
@@ -0,0 +1,70 @@
1
+ import Foundation
2
+
3
+ /// Stable per-install device identifier. Generated once on first call,
4
+ /// persisted alongside the capability cache under the same Application
5
+ /// Support directory. Used for:
6
+ /// - identifying THIS device in mDNS TXT records (LAN discovery).
7
+ /// - identifying THIS device in rendezvous-server pairing payloads.
8
+ /// - keying the capability cache.
9
+ ///
10
+ /// NOT a privacy hazard: the ID is per-install and per-device-storage,
11
+ /// never tied to user identity. Reinstalling the app or wiping app
12
+ /// storage produces a fresh ID — that's the right behaviour and matches
13
+ /// the TS-side `generateDeviceId` semantics in
14
+ /// `packages/dvai-bridge-core/src/capability/deviceId.ts`.
15
+ public final class DeviceIDStore: @unchecked Sendable {
16
+ private let fileURL: URL
17
+ private let lock = NSLock()
18
+ private var cached: String?
19
+
20
+ public init(directory: URL) {
21
+ self.fileURL = directory.appendingPathComponent("device-id.txt", isDirectory: false)
22
+ }
23
+
24
+ /// Return the device ID, generating + persisting it on first call.
25
+ public func get() throws -> String {
26
+ lock.lock()
27
+ defer { lock.unlock() }
28
+ if let cached = cached { return cached }
29
+
30
+ // Try to load existing
31
+ if let data = try? Data(contentsOf: fileURL),
32
+ let raw = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
33
+ !raw.isEmpty {
34
+ cached = raw
35
+ return raw
36
+ }
37
+
38
+ // Generate fresh
39
+ let id = Self.generate()
40
+ try FileManager.default.createDirectory(
41
+ at: fileURL.deletingLastPathComponent(),
42
+ withIntermediateDirectories: true
43
+ )
44
+ try id.write(to: fileURL, atomically: true, encoding: .utf8)
45
+ cached = id
46
+ return id
47
+ }
48
+
49
+ /// Generate a 22-char URL-safe base64 random ID. Mirrors the TS
50
+ /// `generateDeviceId()` shape (16 random bytes, base64url, no
51
+ /// padding) so device IDs round-trip across platforms.
52
+ public static func generate() -> String {
53
+ var bytes = [UInt8](repeating: 0, count: 16)
54
+ let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
55
+ if status != errSecSuccess {
56
+ // Fall back to UUID — still random, just not from SecRandom.
57
+ return UUID().uuidString.replacingOccurrences(of: "-", with: "")
58
+ .lowercased().prefix(22).description
59
+ }
60
+ return base64UrlEncode(Data(bytes))
61
+ }
62
+
63
+ private static func base64UrlEncode(_ data: Data) -> String {
64
+ let b64 = data.base64EncodedString()
65
+ return b64
66
+ .replacingOccurrences(of: "+", with: "-")
67
+ .replacingOccurrences(of: "/", with: "_")
68
+ .replacingOccurrences(of: "=", with: "")
69
+ }
70
+ }
@@ -0,0 +1,41 @@
1
+ import Foundation
2
+
3
+ /// v3.2 — JSON-serializable result of `DVAIBridge.shared.assessHardware()`.
4
+ ///
5
+ /// Returned to consumer code so the app developer can decide whether
6
+ /// to call `DVAIBridge.shared.start(...)` and what (if anything) to
7
+ /// surface in the UI. The SDK itself never shows UI for hardware
8
+ /// decisions — consumer apps make those choices.
9
+ ///
10
+ /// `Codable` so it round-trips cleanly through Capacitor / React
11
+ /// Native / Pigeon bridges as JSON without any custom converter.
12
+ public struct HardwareAssessment: Sendable, Codable, Equatable {
13
+ /// Lifecycle mode the SDK would enter on `start()`. See
14
+ /// `PrecheckMode` for the three values.
15
+ public let mode: PrecheckMode
16
+ /// Estimated decode tok/s for any 1–3B-class model on this device.
17
+ public let tokPerSec: Double
18
+ /// Human-readable explanation; safe to log + display.
19
+ public let reason: String
20
+ /// Underlying hints used to compute the estimate.
21
+ public let hints: DeviceCapabilityHints
22
+
23
+ public init(
24
+ mode: PrecheckMode,
25
+ tokPerSec: Double,
26
+ reason: String,
27
+ hints: DeviceCapabilityHints
28
+ ) {
29
+ self.mode = mode
30
+ self.tokPerSec = tokPerSec
31
+ self.reason = reason
32
+ self.hints = hints
33
+ }
34
+
35
+ init(from result: CapabilityPrecheck.Result) {
36
+ self.mode = result.mode
37
+ self.tokPerSec = result.tokPerSec
38
+ self.reason = result.reason
39
+ self.hints = result.hints
40
+ }
41
+ }