@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.
- package/DVAIBridge.podspec +120 -0
- package/LICENSE +51 -0
- package/Package.swift +104 -0
- package/README.md +199 -0
- package/ios/Sources/DVAIBridge/BackendKind.swift +23 -0
- package/ios/Sources/DVAIBridge/BoundServer.swift +46 -0
- package/ios/Sources/DVAIBridge/Capability/CapabilityCache.swift +85 -0
- package/ios/Sources/DVAIBridge/Capability/CapabilityPrecheck.swift +193 -0
- package/ios/Sources/DVAIBridge/Capability/CapabilityScore.swift +51 -0
- package/ios/Sources/DVAIBridge/Capability/DeviceID.swift +70 -0
- package/ios/Sources/DVAIBridge/Capability/HardwareAssessment.swift +41 -0
- package/ios/Sources/DVAIBridge/DVAIBridge.swift +658 -0
- package/ios/Sources/DVAIBridge/DVAIBridgeConfig.swift +86 -0
- package/ios/Sources/DVAIBridge/DVAIBridgeError.swift +33 -0
- package/ios/Sources/DVAIBridge/Discovery/MDNSPeer.swift +64 -0
- package/ios/Sources/DVAIBridge/Discovery/NWAdvertiser.swift +103 -0
- package/ios/Sources/DVAIBridge/Discovery/NWBrowserDiscovery.swift +212 -0
- package/ios/Sources/DVAIBridge/Internal/BackendSelector.swift +59 -0
- package/ios/Sources/DVAIBridge/Internal/ProgressBroadcaster.swift +84 -0
- package/ios/Sources/DVAIBridge/License/Audience.swift +133 -0
- package/ios/Sources/DVAIBridge/License/Discovery.swift +164 -0
- package/ios/Sources/DVAIBridge/License/LicenseValidator.swift +392 -0
- package/ios/Sources/DVAIBridge/License/PublicKeys.swift +114 -0
- package/ios/Sources/DVAIBridge/License/Types.swift +195 -0
- package/ios/Sources/DVAIBridge/Offload/OffloadConfig.swift +118 -0
- package/ios/Sources/DVAIBridge/Offload/OffloadProxy.swift +604 -0
- package/ios/Sources/DVAIBridge/Offload/OffloadRuntime.swift +98 -0
- package/ios/Sources/DVAIBridge/Pairing/Pairing.swift +125 -0
- package/ios/Sources/DVAIBridge/Pairing/PairingHandshake.swift +141 -0
- package/ios/Sources/DVAIBridge/Pairing/PairingPolicy.swift +162 -0
- package/ios/Sources/DVAIBridge/Pairing/PairingStore.swift +65 -0
- package/ios/Sources/DVAIBridge/ProgressEvent.swift +34 -0
- package/ios/Sources/DVAIBridge/ReactiveState.swift +149 -0
- package/ios/Sources/DVAICoreMLCore/.gitkeep +0 -0
- package/ios/Sources/DVAICoreMLCore/CoreMLBackendError.swift +19 -0
- package/ios/Sources/DVAICoreMLCore/CoreMLHandlers.swift +123 -0
- package/ios/Sources/DVAICoreMLCore/CoreMLPluginState.swift +130 -0
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLEngine.swift +137 -0
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLGenerator.swift +108 -0
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLSampler.swift +96 -0
- package/ios/Sources/DVAICoreMLCore/Internal/CoreMLTokenizer.swift +69 -0
- package/ios/Tests/DVAIBridgeTests/BackendSelectorTests.swift +53 -0
- package/ios/Tests/DVAIBridgeTests/CapabilityPrecheckTests.swift +108 -0
- package/ios/Tests/DVAIBridgeTests/CoreMLEngineTests.swift +18 -0
- package/ios/Tests/DVAIBridgeTests/CoreMLGeneratorShapeTests.swift +11 -0
- package/ios/Tests/DVAIBridgeTests/CoreMLHandlersTests.swift +32 -0
- package/ios/Tests/DVAIBridgeTests/CoreMLPluginStateTests.swift +41 -0
- package/ios/Tests/DVAIBridgeTests/CoreMLSamplerTests.swift +40 -0
- package/ios/Tests/DVAIBridgeTests/CoreMLTokenizerTests.swift +19 -0
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeAPIShapeTests.swift +37 -0
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeConfigTests.swift +52 -0
- package/ios/Tests/DVAIBridgeTests/DVAIBridgeErrorTests.swift +33 -0
- package/ios/Tests/DVAIBridgeTests/LicenseValidatorTests.swift +658 -0
- package/ios/Tests/DVAIBridgeTests/OffloadProxyDecisionTests.swift +156 -0
- package/ios/Tests/DVAIBridgeTests/OffloadTests.swift +339 -0
- package/ios/Tests/DVAIBridgeTests/ProgressBroadcasterTests.swift +69 -0
- package/ios/Tests/DVAIBridgeTests/ProgressEventTests.swift +25 -0
- package/ios/Tests/DVAIBridgeTests/ReactiveStateTests.swift +45 -0
- package/ios/Tests/DVAIBridgeTests/RealModelIntegrationTest.swift +359 -0
- package/package.json +19 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
3
|
+
|
|
4
|
+
# CocoaPods doesn't allow per-subspec `module_name`, so the whole pod compiles
|
|
5
|
+
# into a single Swift module called `DVAIBridge`. Cross-target imports inside
|
|
6
|
+
# our SwiftPM module graph (e.g. `import DVAILlamaCore`, `import Tokenizers`)
|
|
7
|
+
# are wrapped with `#if !COCOAPODS` (see scripts/wrap-cocoapods-imports.py)
|
|
8
|
+
# so they remain real imports under SwiftPM and become no-ops here, where
|
|
9
|
+
# everything is already same-module. SwiftPM consumers continue to resolve
|
|
10
|
+
# the upstream swift-transformers / swift-jinja / swift-collections packages
|
|
11
|
+
# through Package.swift; only CocoaPods consumers see the vendored copies
|
|
12
|
+
# under Vendor/swift-transformers/.
|
|
13
|
+
|
|
14
|
+
Pod::Spec.new do |s|
|
|
15
|
+
s.name = 'DVAIBridge'
|
|
16
|
+
s.version = package['version']
|
|
17
|
+
s.summary = package['description']
|
|
18
|
+
s.license = { :type => 'Custom', :file => '../../LICENSE' }
|
|
19
|
+
s.homepage = 'https://github.com/dvai-global/dvai-bridge'
|
|
20
|
+
s.author = package['author']
|
|
21
|
+
s.source = { :git => 'https://github.com/dvai-global/dvai-bridge.git', :tag => "v#{s.version}" }
|
|
22
|
+
s.platform = :ios, '18.1'
|
|
23
|
+
s.swift_version = '5.9'
|
|
24
|
+
|
|
25
|
+
s.source_files = [
|
|
26
|
+
# DVAI core actor + reactive state
|
|
27
|
+
'ios/Sources/DVAIBridge/**/*.swift',
|
|
28
|
+
# CoreML backend (uses vendored Tokenizers + Hub + Jinja)
|
|
29
|
+
'ios/Sources/DVAICoreMLCore/**/*.swift',
|
|
30
|
+
# Shared HTTP-server / handler-dispatch types, plus llama.cpp backend
|
|
31
|
+
# — copied from sibling *-core packages into Sources/_external/ by
|
|
32
|
+
# prepare_command (CocoaPods' file globs do not follow `..` paths
|
|
33
|
+
# reliably across pod boundaries).
|
|
34
|
+
'Sources/_external/DVAISharedCore/**/*.swift',
|
|
35
|
+
'Sources/_external/DVAILlamaCore/**/*.swift',
|
|
36
|
+
'Sources/_external/DVAILlamaCoreObjC/**/*.{h,mm}',
|
|
37
|
+
# NOTE: DVAIFoundationCore is intentionally NOT in the pod. It uses
|
|
38
|
+
# Apple's FoundationModels framework whose import emits implicit
|
|
39
|
+
# autolink directives for private frameworks (SwiftUICore /
|
|
40
|
+
# UIUtilities / CoreAudioTypes) that non-Apple products cannot link.
|
|
41
|
+
# The Foundation Models backend is therefore SwiftPM-only. Calling
|
|
42
|
+
# `.start(BackendKind.foundation)` under a CocoaPods build throws
|
|
43
|
+
# DVAIBridgeError.backendUnavailable with a clear message.
|
|
44
|
+
# Vendored swift-transformers stack — see Vendor/swift-transformers/ for
|
|
45
|
+
# upstream attributions and the rationale for stripping HubApi.swift.
|
|
46
|
+
'Vendor/swift-transformers/Tokenizers/**/*.swift',
|
|
47
|
+
'Vendor/swift-transformers/Hub/**/*.swift',
|
|
48
|
+
'Vendor/swift-transformers/Jinja/**/*.swift',
|
|
49
|
+
'Vendor/swift-transformers/OrderedCollections/**/*.swift',
|
|
50
|
+
'Vendor/swift-transformers/InternalCollectionsUtilities/**/*.swift',
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Tokenizer fallback configs (gpt2_tokenizer_config.json, t5_..) are looked
|
|
54
|
+
# up via Bundle.module by the vendored Hub.swift. Vendor/.../Hub/BundleModuleShim.swift
|
|
55
|
+
# provides Bundle.module under !SWIFT_PACKAGE so the lookup resolves to the
|
|
56
|
+
# framework's main bundle, where these resources land.
|
|
57
|
+
s.resources = ['Vendor/swift-transformers/Hub/Resources/*.json']
|
|
58
|
+
|
|
59
|
+
# CocoaPods' pod-validator treats the pod as a self-contained tree and
|
|
60
|
+
# refuses `..` paths in `vendored_frameworks`. The xcframeworks are
|
|
61
|
+
# produced by scripts/mac-side-prepare-xcframework.sh in the llama.cpp
|
|
62
|
+
# submodule's build-apple/ directory (so SwiftPM's binaryTarget paths
|
|
63
|
+
# resolve from Package.swift). For CocoaPods we copy them into a
|
|
64
|
+
# pod-local Frameworks/ folder via prepare_command, which runs in the
|
|
65
|
+
# source tree before validation.
|
|
66
|
+
s.prepare_command = <<-SH
|
|
67
|
+
set -e
|
|
68
|
+
XCF_SRC="../dvai-bridge-android-llama-core/android/src/main/cpp/native/llama.cpp/build-apple"
|
|
69
|
+
mkdir -p Frameworks
|
|
70
|
+
if [ -d "$XCF_SRC/llama.xcframework" ]; then
|
|
71
|
+
rm -rf Frameworks/llama.xcframework
|
|
72
|
+
cp -R "$XCF_SRC/llama.xcframework" Frameworks/llama.xcframework
|
|
73
|
+
fi
|
|
74
|
+
if [ -d "$XCF_SRC/mtmd.xcframework" ]; then
|
|
75
|
+
rm -rf Frameworks/mtmd.xcframework
|
|
76
|
+
cp -R "$XCF_SRC/mtmd.xcframework" Frameworks/mtmd.xcframework
|
|
77
|
+
fi
|
|
78
|
+
# Mirror sibling-package source dirs into Sources/_external/ so they're
|
|
79
|
+
# visible to CocoaPods' file glob (it doesn't follow `..` paths).
|
|
80
|
+
rm -rf Sources/_external
|
|
81
|
+
mkdir -p Sources/_external
|
|
82
|
+
cp -R ../dvai-bridge-ios-shared-core/ios/Sources/DVAISharedCore Sources/_external/
|
|
83
|
+
cp -R ../dvai-bridge-ios-llama-core/ios/Sources/DVAILlamaCore Sources/_external/
|
|
84
|
+
cp -R ../dvai-bridge-ios-llama-core/ios/Sources/DVAILlamaCoreObjC Sources/_external/
|
|
85
|
+
# FoundationCore + MLXCore are intentionally NOT mirrored — see
|
|
86
|
+
# source_files comment.
|
|
87
|
+
SH
|
|
88
|
+
|
|
89
|
+
s.vendored_frameworks = [
|
|
90
|
+
'Frameworks/llama.xcframework',
|
|
91
|
+
'Frameworks/mtmd.xcframework',
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
s.frameworks = ['Foundation', 'CoreML', 'AVFoundation', 'CryptoKit']
|
|
95
|
+
|
|
96
|
+
# Vendored swift-collections 1.4.1 + swift-jinja 2.3.5 use:
|
|
97
|
+
# - `package` access level (needs -package-name; SwiftPM auto-sets it)
|
|
98
|
+
# - `@_lifetime(...)` (needs experimental Lifetimes feature)
|
|
99
|
+
# Set these explicitly for the CocoaPods build so the pod compiles
|
|
100
|
+
# with the same source SwiftPM consumes.
|
|
101
|
+
s.pod_target_xcconfig = {
|
|
102
|
+
'OTHER_SWIFT_FLAGS' => '$(inherited) -package-name DVAIBridgeVendored -enable-experimental-feature Lifetimes',
|
|
103
|
+
# Pin the test-app deployment target to our pod's iOS minimum.
|
|
104
|
+
'IPHONEOS_DEPLOYMENT_TARGET' => '18.1',
|
|
105
|
+
# CocoaPods auto-adds `-framework llama -framework mtmd` to the
|
|
106
|
+
# consumer's app target's OTHER_LDFLAGS but NOT to our pod's own
|
|
107
|
+
# framework target — so symbols from the xcframeworks (referenced
|
|
108
|
+
# by LlamaCppBridge.mm) are unresolved at pod-link time. Add them
|
|
109
|
+
# explicitly here. The xcframework integration script already places
|
|
110
|
+
# the right slice in PODS_XCFRAMEWORKS_BUILD_DIR/DVAIBridge before
|
|
111
|
+
# the link step.
|
|
112
|
+
'OTHER_LDFLAGS' => '$(inherited) -framework "llama" -framework "mtmd"',
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Telegraph stays at ~> 0.30 because Building42 publishes 0.40+ as GitHub
|
|
116
|
+
# tags only; CocoaPods trunk caps at 0.30.0. Our usage only touches stable
|
|
117
|
+
# core types (Server / HTTPRequest / HTTPResponse / HTTPStatus / HTTPHeaders),
|
|
118
|
+
# so consumers on either channel get a working build.
|
|
119
|
+
s.dependency 'Telegraph', '~> 0.30'
|
|
120
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Deep Voice Ai Limited - Software License Agreement
|
|
2
|
+
|
|
3
|
+
**Version 1.0.0**
|
|
4
|
+
|
|
5
|
+
This License Agreement governs the use of the DVAI-Bridge software (the "Software"). By downloading, installing, or using the Software, you agree to be bound by the terms of this License.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. LICENSE GRANTS
|
|
10
|
+
|
|
11
|
+
### 1.1 Development and Personal Use (Free Tier)
|
|
12
|
+
Deep Voice Ai Limited ("Licensor") grants you a non-exclusive, non-transferable, royalty-free license to use the Software solely for:
|
|
13
|
+
- Internal development and testing purposes.
|
|
14
|
+
- Non-commercial personal projects.
|
|
15
|
+
- Academic and non-profit research.
|
|
16
|
+
|
|
17
|
+
### 1.2 Commercial Use (Paid Tier)
|
|
18
|
+
Any use of the Software for **Commercial Purposes** requires a separate, paid Commercial License from Licensor. "Commercial Purposes" include:
|
|
19
|
+
- Use in production environments.
|
|
20
|
+
- Integration into revenue-generating products or services.
|
|
21
|
+
- Distribution to third-party customers for a fee.
|
|
22
|
+
- Use by an entity with more than $100,000 USD in annual revenue.
|
|
23
|
+
|
|
24
|
+
To obtain a Commercial License, contact `info@deepvoiceai.co` or visit `https://deepvoiceai.co/licensing`.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. RESTRICTIONS
|
|
29
|
+
Except as expressly permitted, you may not:
|
|
30
|
+
- Sublicense, rent, lease, or resell the Software without express permission.
|
|
31
|
+
- Remove any proprietary notices or branding from the Software.
|
|
32
|
+
- Use the Software for any illegal or malicious purposes.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 3. INTELLECTUAL PROPERTY
|
|
37
|
+
The Software is owned by **Deep Voice Ai Limited** and is protected by copyright and intellectual property laws. This agreement does not transfer ownership of the Software.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 4. NO WARRANTY
|
|
42
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 5. GOVERNING LAW
|
|
47
|
+
This License shall be governed by and construed in accordance with the laws of the jurisdiction where Deep Voice Ai Limited is registered.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
© 2026 Deep Voice Ai Limited. All rights reserved.
|
package/Package.swift
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// swift-tools-version: 6.0
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "DVAIBridge",
|
|
6
|
+
// iOS 18.1 link-time floor: DVAIFoundationCore requires it (the
|
|
7
|
+
// FoundationModels SDK is `@available(iOS 26, *)` at runtime; 18.1 is
|
|
8
|
+
// its link-time minimum). DVAILlamaCore allows 14.0, but the package
|
|
9
|
+
// as a whole takes the highest minimum. CoreML's MLState requires
|
|
10
|
+
// iOS 18 too, so 18.1 covers everything.
|
|
11
|
+
platforms: [.iOS("18.1"), .macOS(.v14)],
|
|
12
|
+
products: [
|
|
13
|
+
.library(name: "DVAIBridge", targets: ["DVAIBridge"]),
|
|
14
|
+
.library(name: "DVAICoreMLCore", targets: ["DVAICoreMLCore"]),
|
|
15
|
+
],
|
|
16
|
+
dependencies: [
|
|
17
|
+
// Path-dep to the cores. Identity is derived from the path's last
|
|
18
|
+
// directory name; each core has its manifest at the package root,
|
|
19
|
+
// so identities are `dvai-bridge-ios-shared-core` /
|
|
20
|
+
// `dvai-bridge-ios-llama-core` / `dvai-bridge-ios-foundation-core` /
|
|
21
|
+
// `dvai-bridge-ios-mlx-core`.
|
|
22
|
+
.package(path: "../dvai-bridge-ios-shared-core"),
|
|
23
|
+
.package(path: "../dvai-bridge-ios-llama-core"),
|
|
24
|
+
.package(path: "../dvai-bridge-ios-foundation-core"),
|
|
25
|
+
.package(path: "../dvai-bridge-ios-mlx-core"),
|
|
26
|
+
// swift-transformers — provides Tokenizers product for HuggingFace
|
|
27
|
+
// tokenizer loading. Constraint relaxed to `from: 1.2.0` to keep
|
|
28
|
+
// our resolver compatible with mlx-swift-lm 2.x (which pins
|
|
29
|
+
// swift-transformers `<1.3.0`). Our usage of AutoTokenizer.from(modelFolder:)
|
|
30
|
+
// / applyChatTemplate / encode / decode is API-compatible across
|
|
31
|
+
// 1.2.x → 1.3.x.
|
|
32
|
+
.package(url: "https://github.com/huggingface/swift-transformers.git", from: "1.2.0"),
|
|
33
|
+
// v3.2.0 — Hummingbird is the iOS HTTP server backbone (replaces
|
|
34
|
+
// Telegraph). DVAISharedCore exports it transitively via its
|
|
35
|
+
// HttpServer actor, but we pull it here too so the OffloadProxy
|
|
36
|
+
// can use it directly without a `@_implementationOnly` import
|
|
37
|
+
// gymnastics step.
|
|
38
|
+
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
|
|
39
|
+
// v3.2.2 — JWTKit drives the offline JWT license validator
|
|
40
|
+
// (ES256 ECDSA P-256). Refuses alg=none / HMAC to defend against
|
|
41
|
+
// algorithm-confusion attacks. Same .jwt format as the JS-side
|
|
42
|
+
// validator in @dvai-bridge/core.
|
|
43
|
+
.package(url: "https://github.com/vapor/jwt-kit.git", from: "5.5.0"),
|
|
44
|
+
],
|
|
45
|
+
targets: [
|
|
46
|
+
.target(
|
|
47
|
+
name: "DVAICoreMLCore",
|
|
48
|
+
dependencies: [
|
|
49
|
+
.product(name: "Tokenizers", package: "swift-transformers"),
|
|
50
|
+
// CoreML backend uses shared HTTP types — DOES NOT depend on
|
|
51
|
+
// DVAILlamaCore (so CoreML-only consumers don't transitively
|
|
52
|
+
// pull llama.xcframework). DVAISharedCore brings Hummingbird
|
|
53
|
+
// transitively as of v3.2.0.
|
|
54
|
+
.product(name: "DVAISharedCore", package: "dvai-bridge-ios-shared-core"),
|
|
55
|
+
],
|
|
56
|
+
path: "ios/Sources/DVAICoreMLCore",
|
|
57
|
+
// Keep Swift 5 language mode: the existing code in this
|
|
58
|
+
// package was written before Swift 6's strict-concurrency
|
|
59
|
+
// checks landed. Bumping `swift-tools-version` to 6.0 (to
|
|
60
|
+
// pull in JWTKit 5.x) doesn't require flipping the language
|
|
61
|
+
// mode; we opt out per-target to keep the source unchanged.
|
|
62
|
+
swiftSettings: [.swiftLanguageMode(.v5)]
|
|
63
|
+
),
|
|
64
|
+
.target(
|
|
65
|
+
name: "DVAIBridge",
|
|
66
|
+
dependencies: [
|
|
67
|
+
// DVAIBridge depends on all four backends + shared types.
|
|
68
|
+
// The llama-core dep is what brings in ModelDownloader; if a
|
|
69
|
+
// future refactor extracts that too, DVAIBridge could drop
|
|
70
|
+
// the llama-core dep when the consumer is using only
|
|
71
|
+
// .mlx / .foundation / .coreml.
|
|
72
|
+
.product(name: "DVAISharedCore", package: "dvai-bridge-ios-shared-core"),
|
|
73
|
+
.product(name: "DVAILlamaCore", package: "dvai-bridge-ios-llama-core"),
|
|
74
|
+
.product(name: "DVAIFoundationCore", package: "dvai-bridge-ios-foundation-core"),
|
|
75
|
+
.product(name: "DVAIMLXCore", package: "dvai-bridge-ios-mlx-core"),
|
|
76
|
+
"DVAICoreMLCore",
|
|
77
|
+
// v3.2 Phase 5 — outgoing-offload pre-routing proxy.
|
|
78
|
+
// Hummingbird (built on swift-nio) gives us proper
|
|
79
|
+
// streaming SSE bodies through the proxy.
|
|
80
|
+
.product(name: "Hummingbird", package: "hummingbird"),
|
|
81
|
+
// v3.2.2 — offline JWT license validator.
|
|
82
|
+
.product(name: "JWTKit", package: "jwt-kit"),
|
|
83
|
+
],
|
|
84
|
+
path: "ios/Sources/DVAIBridge",
|
|
85
|
+
swiftSettings: [.swiftLanguageMode(.v5)]
|
|
86
|
+
),
|
|
87
|
+
.testTarget(
|
|
88
|
+
name: "DVAIBridgeTests",
|
|
89
|
+
dependencies: [
|
|
90
|
+
"DVAIBridge",
|
|
91
|
+
.product(name: "DVAISharedCore", package: "dvai-bridge-ios-shared-core"),
|
|
92
|
+
.product(name: "DVAILlamaCore", package: "dvai-bridge-ios-llama-core"),
|
|
93
|
+
.product(name: "DVAIFoundationCore", package: "dvai-bridge-ios-foundation-core"),
|
|
94
|
+
.product(name: "DVAIMLXCore", package: "dvai-bridge-ios-mlx-core"),
|
|
95
|
+
"DVAICoreMLCore",
|
|
96
|
+
// v3.2.2 — license validator tests sign their own test
|
|
97
|
+
// tokens with a freshly generated test ES256 keypair.
|
|
98
|
+
.product(name: "JWTKit", package: "jwt-kit"),
|
|
99
|
+
],
|
|
100
|
+
path: "ios/Tests/DVAIBridgeTests",
|
|
101
|
+
swiftSettings: [.swiftLanguageMode(.v5)]
|
|
102
|
+
),
|
|
103
|
+
]
|
|
104
|
+
)
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# DVAI-Bridge
|
|
4
|
+
|
|
5
|
+
<!-- [](https://github.com/Westenets/dvai-bridge/actions/workflows/smoke-real-models.yml) -->
|
|
6
|
+
|
|
7
|
+
[](LICENSE)      
|
|
8
|
+
|
|
9
|
+
> **The local OpenAI server you embed inside your app.**
|
|
10
|
+
> One library. One HTTP wire. Every platform. Zero install for your users.
|
|
11
|
+
|
|
12
|
+
**Docs:** [dvai-bridge.deepvoiceai.co](https://dvai-bridge.deepvoiceai.co)
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { DVAI } from "@dvai-bridge/core";
|
|
16
|
+
import OpenAI from "openai";
|
|
17
|
+
|
|
18
|
+
const dvai = new DVAI({ backend: "transformers" });
|
|
19
|
+
await dvai.initialize();
|
|
20
|
+
|
|
21
|
+
const openai = new OpenAI({ baseURL: dvai.baseUrl, apiKey: "ignored" });
|
|
22
|
+
await openai.chat.completions.create({
|
|
23
|
+
model: dvai.transformersModelId,
|
|
24
|
+
messages: [{ role: "user", content: "Hello!" }],
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it. A real OpenAI-compatible server is now running inside your app's
|
|
29
|
+
own process. Point any OpenAI client — LangChain, the OpenAI SDK, the Vercel
|
|
30
|
+
AI SDK, anything — at `dvai.baseUrl` and your agent code keeps working.
|
|
31
|
+
|
|
32
|
+
Built by **[Deep Voice AI](https://deepvoiceai.co)**.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Why it exists
|
|
37
|
+
|
|
38
|
+
Local AI works beautifully on a laptop with **Ollama + LangChain**. Then you
|
|
39
|
+
try to ship the app and your users don't have Ollama. Mobile can't run it.
|
|
40
|
+
Corporate IT won't add another daemon. So you reinvent the same plumbing —
|
|
41
|
+
spawn an inference engine, bind a port, translate to OpenAI HTTP, handle
|
|
42
|
+
CORS, manage lifecycle, wrap the accelerator of the day per platform — and
|
|
43
|
+
do it all over again for every target OS.
|
|
44
|
+
|
|
45
|
+
DVAI-Bridge is that plumbing, packaged as a library, for every client
|
|
46
|
+
platform.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## What you get
|
|
51
|
+
|
|
52
|
+
- **One OpenAI HTTP surface.** Bound on `127.0.0.1` (or `0.0.0.0` for
|
|
53
|
+
device-to-device). Streaming, embeddings, models, recovery — all built in.
|
|
54
|
+
- **Six SDKs.** `@dvai-bridge/core` + `react` + `vanilla` + `capacitor`,
|
|
55
|
+
`DVAIBridge` (Swift / iOS), `co.deepvoiceai:dvai-bridge` (Kotlin / Android),
|
|
56
|
+
`@dvai-bridge/react-native`, `dvai_bridge` (Flutter), `co.deepvoiceai.dvai-bridge` (.NET).
|
|
57
|
+
- **Nine backends.** WebLLM, Transformers.js, llama.cpp, Apple Foundation
|
|
58
|
+
Models, MLX, CoreML / ANE, MediaPipe LLM, LiteRT, ONNX Runtime GenAI —
|
|
59
|
+
selected per-platform, invisible to your agent code.
|
|
60
|
+
- **Native acceleration** wherever it runs: WebGPU in browsers, CUDA / Metal
|
|
61
|
+
/ Vulkan / DirectML on desktop, ANE / Metal / MLX on iOS, NNAPI / QNN
|
|
62
|
+
Hexagon / GPU delegate on Android.
|
|
63
|
+
- **Multimodal.** Text, image, audio, video — declarative loader for
|
|
64
|
+
cutting-edge models (Gemma 4, LLaVA, Idefics) without waiting for library
|
|
65
|
+
updates.
|
|
66
|
+
- **Distributed inference (v3.0+).** Phone too slow? Offload to your laptop
|
|
67
|
+
on the same Wi-Fi via mDNS pairing — same OpenAI wire, transparent to
|
|
68
|
+
your code. Internet path via a self-hostable rendezvous server.
|
|
69
|
+
- **DVAI Hub (v3.1+).** A first-party desktop utility that turns any device
|
|
70
|
+
into a strong-peer for the rest of your fleet. Brand-neutral install via
|
|
71
|
+
Homebrew / winget / GitHub Releases, OR fork it for your own branded
|
|
72
|
+
companion. Routes through Ollama / LM Studio / vLLM / llama-server /
|
|
73
|
+
llamafile if you've already got those running.
|
|
74
|
+
- **Zero user install.** It's a library, not a daemon. `npm install`,
|
|
75
|
+
`cocoapods`, gradle — your CI already has the muscle for it.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Supported platforms
|
|
80
|
+
|
|
81
|
+
| Stack | Package | Backends |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| Browser (React, Vue, Svelte, vanilla JS) | `@dvai-bridge/core` + `react` / `vanilla` | WebLLM (WebGPU), Transformers.js (WebGPU / WASM SIMD) |
|
|
84
|
+
| Node / Bun / Electron | `@dvai-bridge/core` | Transformers.js, native llama.cpp |
|
|
85
|
+
| Capacitor hybrid mobile | `@dvai-bridge/capacitor` + backend slice | Native llama.cpp (Metal iOS, Vulkan / CPU Android) |
|
|
86
|
+
| iOS native (Swift) | `DVAIBridge` (SPM / CocoaPods) | llama.cpp (Metal), CoreML / ANE, Apple Foundation Models, MLX |
|
|
87
|
+
| Android native (Kotlin / Java) | `co.deepvoiceai:dvai-bridge` (AAR) | llama.cpp, MediaPipe LLM, LiteRT, NNAPI / QNN |
|
|
88
|
+
| React Native (≥0.77, TurboModule) | `@dvai-bridge/react-native` | All iOS + Android backends (delegates) |
|
|
89
|
+
| Flutter (≥3.39) | `dvai_bridge` (pub.dev) | All iOS + Android backends (Pigeon channels) |
|
|
90
|
+
| .NET 10 LTS (MAUI / Avalonia / WinUI / Catalyst / desktop) | `co.deepvoiceai.dvai-bridge*` (NuGet) | iOS / Android delegate to native; desktop = llama.cpp + ONNX Runtime GenAI + ML.NET |
|
|
91
|
+
|
|
92
|
+
Full quickstart per platform: [dvai-bridge.deepvoiceai.co/guide/getting-started](https://dvai-bridge.deepvoiceai.co/guide/getting-started)
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// React
|
|
100
|
+
import { DVAIProvider, useDVAI } from "@dvai-bridge/react";
|
|
101
|
+
<DVAIProvider config={{ backend: "transformers" }}>
|
|
102
|
+
<Chat />
|
|
103
|
+
</DVAIProvider>;
|
|
104
|
+
function Chat() {
|
|
105
|
+
const { isReady, baseUrl } = useDVAI();
|
|
106
|
+
return isReady ? <div>Local AI live at {baseUrl}</div> : <Loading />;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```swift
|
|
111
|
+
// iOS
|
|
112
|
+
let server = try await DVAIBridge.shared.start()
|
|
113
|
+
// server.baseUrl = "http://127.0.0.1:38883/v1"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```kotlin
|
|
117
|
+
// Android
|
|
118
|
+
val server = DVAIBridge.start(context)
|
|
119
|
+
// server.baseUrl = "http://127.0.0.1:38883/v1"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```dart
|
|
123
|
+
// Flutter
|
|
124
|
+
final state = await DVAIBridge.instance.start(
|
|
125
|
+
backend: BackendKind.auto,
|
|
126
|
+
modelPath: '/path/to/model.gguf',
|
|
127
|
+
);
|
|
128
|
+
// state.baseUrl = "http://127.0.0.1:38883/v1"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```csharp
|
|
132
|
+
// .NET
|
|
133
|
+
var server = await DVAIBridge.Shared.StartAsync(new StartOptions {
|
|
134
|
+
Backend = BackendKind.Auto,
|
|
135
|
+
ModelPath = "/path/to/model.gguf",
|
|
136
|
+
});
|
|
137
|
+
// server.BaseUrl = "http://127.0.0.1:38883/v1"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Multimodal, streaming, embeddings, distributed offload, the Hub —
|
|
141
|
+
everything's at the [docs site](https://dvai-bridge.deepvoiceai.co).
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## What's new in v3.1
|
|
146
|
+
|
|
147
|
+
- **DVAI Hub** — Tauri desktop utility that's the strong-peer side of v3
|
|
148
|
+
distributed inference. `brew install deepvoiceai/dvai-hub/dvai-hub` (or
|
|
149
|
+
`winget install DeepVoiceAI.DVAIHub`) → mobile apps on the same Wi-Fi
|
|
150
|
+
pair with it and offload heavy inference. [Guide →](https://dvai-bridge.deepvoiceai.co/guide/dvai-hub)
|
|
151
|
+
- **External-engine bridge.** Hub surfaces Ollama / LM Studio / vLLM /
|
|
152
|
+
llama-server / llamafile as additional backend pools so paired apps
|
|
153
|
+
serve from whatever's already cached. Opt-in per engine.
|
|
154
|
+
- **Strict substitution policy.** Models with mismatched family / version /
|
|
155
|
+
size / type are refused by default; quant-only mismatches gated behind a
|
|
156
|
+
per-pairing `preferBetterQuant` flag. No silent mis-routing.
|
|
157
|
+
- **HMAC-signed identity** on `/v1/chat/completions`. Per-app audit logs
|
|
158
|
+
surface who served what, with structured `(appId, peerDeviceId,
|
|
159
|
+
engine, requestedModel, servedModel, outcome)` rows.
|
|
160
|
+
- **Library finalization.** `httpBindHost` (LAN bind), `chatCompletionInterceptor`
|
|
161
|
+
(extension point), HMAC primitives re-exported, `/v1/dvai/*` routes
|
|
162
|
+
actually dispatched, TransformersBackend Node-mode device fix.
|
|
163
|
+
[Migration v3.0 → v3.1 →](https://dvai-bridge.deepvoiceai.co/migration/v3.0-to-v3.1)
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Robustness
|
|
168
|
+
|
|
169
|
+
Streaming-correct (SSE passthrough + blank-chunk detection), generation
|
|
170
|
+
timeout, automatic engine-state recovery on fatal errors, port fallback,
|
|
171
|
+
worker offloading, Private Network Access ready, CORS configured. The
|
|
172
|
+
boring substrate so your agent code never has to think about it.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Licensing
|
|
177
|
+
|
|
178
|
+
Dual: **free for development & personal use** on `localhost` (verified at
|
|
179
|
+
runtime). **Commercial use** requires a license key — `info@deepvoiceai.co`.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
PRs welcome.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pnpm install
|
|
189
|
+
pnpm build
|
|
190
|
+
bash scripts/build-all.sh # full matrix (auto-skips per-host)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
[`CONTRIBUTING.md`](./CONTRIBUTING.md) for the PR flow. Per-platform
|
|
194
|
+
contributor docs (iOS / Android / RN / Flutter / .NET) under
|
|
195
|
+
[`docs/development/`](./docs/development/).
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
© Deep Voice AI Limited. All rights reserved.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Inference backend used by `DVAIBridge.shared.start(...)`.
|
|
4
|
+
public enum BackendKind: String, Sendable, Codable, CaseIterable {
|
|
5
|
+
/// Resolve the best available backend at runtime.
|
|
6
|
+
case auto
|
|
7
|
+
/// llama.cpp via Metal (iOS) — the broad-compatibility default.
|
|
8
|
+
case llama
|
|
9
|
+
/// Apple Foundation Models (LanguageModelSession). Requires iOS 26+ at runtime.
|
|
10
|
+
/// SwiftPM-only: omitted from CocoaPods builds (the `import FoundationModels`
|
|
11
|
+
/// auto-link directive references private frameworks CocoaPods consumers
|
|
12
|
+
/// cannot link). Selecting this under CocoaPods throws `backendUnavailable`.
|
|
13
|
+
case foundation
|
|
14
|
+
/// CoreML / Apple Neural Engine via `MLModel` + `MLState` (iOS 18+).
|
|
15
|
+
case coreml
|
|
16
|
+
/// MLX — Apple Silicon GPU/Neural Engine via `mlx-swift-lm`. Apple-Silicon
|
|
17
|
+
/// only at runtime; the iOS Simulator on Intel hosts has no MLX device.
|
|
18
|
+
/// Loads HuggingFace MLX-converted checkpoints (e.g.
|
|
19
|
+
/// "mlx-community/Llama-3.2-1B-Instruct-4bit"). SwiftPM-only for the
|
|
20
|
+
/// same reasons as `.foundation` — the mlx-swift-lm transitive deps
|
|
21
|
+
/// don't publish CocoaPods specs.
|
|
22
|
+
case mlx
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public struct BoundServer: Sendable, Equatable {
|
|
4
|
+
public let baseUrl: String
|
|
5
|
+
public let port: Int
|
|
6
|
+
public let backend: BackendKind
|
|
7
|
+
public let modelId: String
|
|
8
|
+
/// v3.2.2 — the license status the validator reported at startup.
|
|
9
|
+
/// `nil` when `DVAIBridge.start(_:)` was called without going through
|
|
10
|
+
/// the license gate (e.g. legacy test fixtures). `commercial` / `trial`
|
|
11
|
+
/// on the paid path; `freeDev` on debug / simulator. The two failure
|
|
12
|
+
/// cases (`freeProd` / `freeExpired`) never make it here because
|
|
13
|
+
/// `start(_:)` throws `LicenseRequiredError` before constructing
|
|
14
|
+
/// `BoundServer`.
|
|
15
|
+
public let licenseStatus: LicenseStatus?
|
|
16
|
+
|
|
17
|
+
public init(
|
|
18
|
+
baseUrl: String,
|
|
19
|
+
port: Int,
|
|
20
|
+
backend: BackendKind,
|
|
21
|
+
modelId: String,
|
|
22
|
+
licenseStatus: LicenseStatus? = nil
|
|
23
|
+
) {
|
|
24
|
+
self.baseUrl = baseUrl
|
|
25
|
+
self.port = port
|
|
26
|
+
self.backend = backend
|
|
27
|
+
self.modelId = modelId
|
|
28
|
+
self.licenseStatus = licenseStatus
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Construct from the underlying core PluginState's `[String: Any]` result.
|
|
32
|
+
internal init(coreResult: [String: Any], backend: BackendKind, licenseStatus: LicenseStatus? = nil) throws {
|
|
33
|
+
guard let baseUrl = coreResult["baseUrl"] as? String,
|
|
34
|
+
let port = (coreResult["port"] as? Int) ?? (coreResult["port"] as? NSNumber)?.intValue
|
|
35
|
+
else {
|
|
36
|
+
throw DVAIBridgeError.backendError(underlying: "core PluginState returned malformed start result")
|
|
37
|
+
}
|
|
38
|
+
let modelId = (coreResult["modelId"] as? String) ?? ""
|
|
39
|
+
self.init(baseUrl: baseUrl, port: port, backend: backend, modelId: modelId, licenseStatus: licenseStatus)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// v3.2.2 — derive a copy with the license status attached.
|
|
43
|
+
internal func with(licenseStatus: LicenseStatus?) -> BoundServer {
|
|
44
|
+
BoundServer(baseUrl: baseUrl, port: port, backend: backend, modelId: modelId, licenseStatus: licenseStatus)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// JSON-file-backed capability cache living under
|
|
4
|
+
/// `Application Support/dvai-bridge/capability.json`. Mirrors the TS-side
|
|
5
|
+
/// `NodeFsCapabilityCache` in `packages/dvai-bridge-core/src/capability/cache.ts`
|
|
6
|
+
/// so the same on-disk format round-trips between native and JS layers
|
|
7
|
+
/// running on the same Mac (Mac Catalyst / Electron).
|
|
8
|
+
public actor CapabilityCache {
|
|
9
|
+
private let fileURL: URL
|
|
10
|
+
private var cache: [String: CapabilityScore]?
|
|
11
|
+
|
|
12
|
+
public init(directory: URL) {
|
|
13
|
+
self.fileURL = directory.appendingPathComponent("capability.json", isDirectory: false)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public func get(_ key: CapabilityCacheKey) async -> CapabilityScore? {
|
|
17
|
+
let map = await load()
|
|
18
|
+
return map[Self.diskKey(key)]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public func set(_ score: CapabilityScore) async throws {
|
|
22
|
+
var map = await load()
|
|
23
|
+
let key = CapabilityCacheKey(modelId: score.modelId, libraryVersion: score.libraryVersion)
|
|
24
|
+
map[Self.diskKey(key)] = score
|
|
25
|
+
cache = map
|
|
26
|
+
try await save()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public func list() async -> [CapabilityScore] {
|
|
30
|
+
let map = await load()
|
|
31
|
+
return Array(map.values)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public func clear() async throws {
|
|
35
|
+
cache = [:]
|
|
36
|
+
try await save()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private func load() async -> [String: CapabilityScore] {
|
|
40
|
+
if let cache = cache { return cache }
|
|
41
|
+
if let data = try? Data(contentsOf: fileURL),
|
|
42
|
+
let decoded = try? JSONDecoder().decode([String: CapabilityScore].self, from: data) {
|
|
43
|
+
cache = decoded
|
|
44
|
+
return decoded
|
|
45
|
+
}
|
|
46
|
+
cache = [:]
|
|
47
|
+
return [:]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func save() async throws {
|
|
51
|
+
guard let cache = cache else { return }
|
|
52
|
+
try FileManager.default.createDirectory(
|
|
53
|
+
at: fileURL.deletingLastPathComponent(),
|
|
54
|
+
withIntermediateDirectories: true
|
|
55
|
+
)
|
|
56
|
+
let encoder = JSONEncoder()
|
|
57
|
+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
58
|
+
let data = try encoder.encode(cache)
|
|
59
|
+
try data.write(to: fileURL, options: .atomic)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Cache key on-disk: `${modelId}|${libraryVersion}`.
|
|
63
|
+
static func diskKey(_ key: CapabilityCacheKey) -> String {
|
|
64
|
+
"\(key.modelId)|\(key.libraryVersion)"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Resolves the canonical Application Support directory for dvai-bridge
|
|
69
|
+
/// caches (capability + pairings + device-id). Matches the iOS / Mac
|
|
70
|
+
/// Catalyst path documented in
|
|
71
|
+
/// `docs/migration/v2.4-to-v3.0.md` (Operational notes).
|
|
72
|
+
public enum DVAIBridgeSupportDirectory {
|
|
73
|
+
public static func resolve() throws -> URL {
|
|
74
|
+
let fm = FileManager.default
|
|
75
|
+
let base = try fm.url(
|
|
76
|
+
for: .applicationSupportDirectory,
|
|
77
|
+
in: .userDomainMask,
|
|
78
|
+
appropriateFor: nil,
|
|
79
|
+
create: true
|
|
80
|
+
)
|
|
81
|
+
let dir = base.appendingPathComponent("dvai-bridge", isDirectory: true)
|
|
82
|
+
try fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
83
|
+
return dir
|
|
84
|
+
}
|
|
85
|
+
}
|