@dvai-bridge/ios-llama-core 4.0.0 → 4.0.1
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/LICENSE +341 -34
- package/Package.swift +71 -71
- package/ios/Sources/DVAILlamaCore/AudioDecoder.swift +112 -112
- package/ios/Sources/DVAILlamaCore/ContentPartsTranslator.swift +232 -232
- package/ios/Sources/DVAILlamaCore/ImageDecoder.swift +91 -91
- package/ios/Sources/DVAILlamaCore/LlamaCppBridgeProtocol.swift +59 -59
- package/ios/Sources/DVAILlamaCore/LlamaHandlers.swift +422 -422
- package/ios/Sources/DVAILlamaCore/ModelDownloader.swift +445 -445
- package/ios/Sources/DVAILlamaCore/PluginState.swift +158 -158
- package/ios/Sources/DVAILlamaCoreObjC/LlamaCppBridge.mm +649 -649
- package/ios/Sources/DVAILlamaCoreObjC/include/LlamaCppBridge.h +101 -101
- package/ios/Tests/DVAILlamaCoreTests/AudioDecoderTest.swift +46 -46
- package/ios/Tests/DVAILlamaCoreTests/ContentPartsTranslatorTest.swift +361 -361
- package/ios/Tests/DVAILlamaCoreTests/ImageDecoderTest.swift +139 -139
- package/ios/Tests/DVAILlamaCoreTests/LlamaCppBridgeTest.swift +131 -131
- package/ios/Tests/DVAILlamaCoreTests/LlamaHandlersTest.swift +515 -515
- package/ios/Tests/DVAILlamaCoreTests/ModelDownloaderTest.swift +89 -89
- package/ios/Tests/DVAILlamaCoreTests/PluginStateTest.swift +51 -51
- package/package.json +3 -3
- package/README.md +0 -199
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
import CryptoKit
|
|
3
|
-
@testable import DVAILlamaCore
|
|
4
|
-
|
|
5
|
-
final class ModelDownloaderTest: XCTestCase {
|
|
6
|
-
/// Per-test cache dir so tests don't pollute the real App Support folder.
|
|
7
|
-
private var tmpCacheDir: URL!
|
|
8
|
-
private var downloader: ModelDownloader!
|
|
9
|
-
|
|
10
|
-
override func setUp() {
|
|
11
|
-
super.setUp()
|
|
12
|
-
let base = FileManager.default.temporaryDirectory
|
|
13
|
-
tmpCacheDir = base.appendingPathComponent("dvai-modeldownloader-test-\(UUID().uuidString)")
|
|
14
|
-
downloader = ModelDownloader(cacheDirOverride: tmpCacheDir)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
override func tearDown() {
|
|
18
|
-
if let dir = tmpCacheDir {
|
|
19
|
-
try? FileManager.default.removeItem(at: dir)
|
|
20
|
-
}
|
|
21
|
-
downloader = nil
|
|
22
|
-
tmpCacheDir = nil
|
|
23
|
-
super.tearDown()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/// Calling `cacheDirURL()` should create the directory if missing and
|
|
27
|
-
/// return a path that resolves under the override.
|
|
28
|
-
func testCacheDirCreates() async throws {
|
|
29
|
-
let url = try await downloader.cacheDirURL()
|
|
30
|
-
XCTAssertEqual(url.path, tmpCacheDir.path)
|
|
31
|
-
var isDir: ObjCBool = false
|
|
32
|
-
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir))
|
|
33
|
-
XCTAssertTrue(isDir.boolValue)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// Cache hit: writing a known file with a known sha256 to the cache dir
|
|
37
|
-
/// then calling `downloadModel(...)` with that sha must return
|
|
38
|
-
/// `cached: true` without ever touching the network — proven by passing
|
|
39
|
-
/// a deliberately broken URL.
|
|
40
|
-
func testCacheHitReturnsCached() async throws {
|
|
41
|
-
let dir = try await downloader.cacheDirURL()
|
|
42
|
-
let filename = "fixture.bin"
|
|
43
|
-
let payload = "hello, dvai cache!".data(using: .utf8)!
|
|
44
|
-
try payload.write(to: dir.appendingPathComponent(filename))
|
|
45
|
-
|
|
46
|
-
let digest = SHA256.hash(data: payload)
|
|
47
|
-
let hex = digest.map { String(format: "%02x", $0) }.joined()
|
|
48
|
-
|
|
49
|
-
// URL is intentionally bogus — a real network call would fail. The
|
|
50
|
-
// cache-hit fast path bypasses network entirely.
|
|
51
|
-
let bogusURL = URL(string: "https://invalid.dvai.test/should-not-fetch.bin")!
|
|
52
|
-
let result = try await downloader.downloadModel(
|
|
53
|
-
url: bogusURL,
|
|
54
|
-
expectedSha256: hex,
|
|
55
|
-
destFilename: filename,
|
|
56
|
-
headers: [:],
|
|
57
|
-
onProgress: { _, _ in }
|
|
58
|
-
)
|
|
59
|
-
XCTAssertTrue(result.cached, "expected cache-hit short-circuit")
|
|
60
|
-
XCTAssertEqual(result.path, dir.appendingPathComponent(filename).path)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/// `listCachedModels()` enumerates regular files (skipping `.partial`
|
|
64
|
-
/// and dotfiles) and `deleteCachedModel(...)` removes them.
|
|
65
|
-
func testListAndDelete() async throws {
|
|
66
|
-
let dir = try await downloader.cacheDirURL()
|
|
67
|
-
let a = "alpha".data(using: .utf8)!
|
|
68
|
-
let b = "bravo".data(using: .utf8)!
|
|
69
|
-
try a.write(to: dir.appendingPathComponent("a.gguf"))
|
|
70
|
-
try b.write(to: dir.appendingPathComponent("b.gguf"))
|
|
71
|
-
// Files that must be ignored:
|
|
72
|
-
try Data().write(to: dir.appendingPathComponent("c.gguf.partial"))
|
|
73
|
-
try Data().write(to: dir.appendingPathComponent(".hidden"))
|
|
74
|
-
|
|
75
|
-
let listed = try await downloader.listCachedModels()
|
|
76
|
-
let names = Set(listed.map { $0.filename })
|
|
77
|
-
XCTAssertEqual(names, ["a.gguf", "b.gguf"])
|
|
78
|
-
XCTAssertEqual(listed.count, 2)
|
|
79
|
-
// Bytes + sha255 are populated.
|
|
80
|
-
for info in listed {
|
|
81
|
-
XCTAssertGreaterThan(info.bytes, 0)
|
|
82
|
-
XCTAssertEqual(info.sha256.count, 64)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
try await downloader.deleteCachedModel(filename: "a.gguf")
|
|
86
|
-
let listed2 = try await downloader.listCachedModels()
|
|
87
|
-
XCTAssertEqual(Set(listed2.map { $0.filename }), ["b.gguf"])
|
|
88
|
-
}
|
|
89
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
import CryptoKit
|
|
3
|
+
@testable import DVAILlamaCore
|
|
4
|
+
|
|
5
|
+
final class ModelDownloaderTest: XCTestCase {
|
|
6
|
+
/// Per-test cache dir so tests don't pollute the real App Support folder.
|
|
7
|
+
private var tmpCacheDir: URL!
|
|
8
|
+
private var downloader: ModelDownloader!
|
|
9
|
+
|
|
10
|
+
override func setUp() {
|
|
11
|
+
super.setUp()
|
|
12
|
+
let base = FileManager.default.temporaryDirectory
|
|
13
|
+
tmpCacheDir = base.appendingPathComponent("dvai-modeldownloader-test-\(UUID().uuidString)")
|
|
14
|
+
downloader = ModelDownloader(cacheDirOverride: tmpCacheDir)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override func tearDown() {
|
|
18
|
+
if let dir = tmpCacheDir {
|
|
19
|
+
try? FileManager.default.removeItem(at: dir)
|
|
20
|
+
}
|
|
21
|
+
downloader = nil
|
|
22
|
+
tmpCacheDir = nil
|
|
23
|
+
super.tearDown()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Calling `cacheDirURL()` should create the directory if missing and
|
|
27
|
+
/// return a path that resolves under the override.
|
|
28
|
+
func testCacheDirCreates() async throws {
|
|
29
|
+
let url = try await downloader.cacheDirURL()
|
|
30
|
+
XCTAssertEqual(url.path, tmpCacheDir.path)
|
|
31
|
+
var isDir: ObjCBool = false
|
|
32
|
+
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir))
|
|
33
|
+
XCTAssertTrue(isDir.boolValue)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Cache hit: writing a known file with a known sha256 to the cache dir
|
|
37
|
+
/// then calling `downloadModel(...)` with that sha must return
|
|
38
|
+
/// `cached: true` without ever touching the network — proven by passing
|
|
39
|
+
/// a deliberately broken URL.
|
|
40
|
+
func testCacheHitReturnsCached() async throws {
|
|
41
|
+
let dir = try await downloader.cacheDirURL()
|
|
42
|
+
let filename = "fixture.bin"
|
|
43
|
+
let payload = "hello, dvai cache!".data(using: .utf8)!
|
|
44
|
+
try payload.write(to: dir.appendingPathComponent(filename))
|
|
45
|
+
|
|
46
|
+
let digest = SHA256.hash(data: payload)
|
|
47
|
+
let hex = digest.map { String(format: "%02x", $0) }.joined()
|
|
48
|
+
|
|
49
|
+
// URL is intentionally bogus — a real network call would fail. The
|
|
50
|
+
// cache-hit fast path bypasses network entirely.
|
|
51
|
+
let bogusURL = URL(string: "https://invalid.dvai.test/should-not-fetch.bin")!
|
|
52
|
+
let result = try await downloader.downloadModel(
|
|
53
|
+
url: bogusURL,
|
|
54
|
+
expectedSha256: hex,
|
|
55
|
+
destFilename: filename,
|
|
56
|
+
headers: [:],
|
|
57
|
+
onProgress: { _, _ in }
|
|
58
|
+
)
|
|
59
|
+
XCTAssertTrue(result.cached, "expected cache-hit short-circuit")
|
|
60
|
+
XCTAssertEqual(result.path, dir.appendingPathComponent(filename).path)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// `listCachedModels()` enumerates regular files (skipping `.partial`
|
|
64
|
+
/// and dotfiles) and `deleteCachedModel(...)` removes them.
|
|
65
|
+
func testListAndDelete() async throws {
|
|
66
|
+
let dir = try await downloader.cacheDirURL()
|
|
67
|
+
let a = "alpha".data(using: .utf8)!
|
|
68
|
+
let b = "bravo".data(using: .utf8)!
|
|
69
|
+
try a.write(to: dir.appendingPathComponent("a.gguf"))
|
|
70
|
+
try b.write(to: dir.appendingPathComponent("b.gguf"))
|
|
71
|
+
// Files that must be ignored:
|
|
72
|
+
try Data().write(to: dir.appendingPathComponent("c.gguf.partial"))
|
|
73
|
+
try Data().write(to: dir.appendingPathComponent(".hidden"))
|
|
74
|
+
|
|
75
|
+
let listed = try await downloader.listCachedModels()
|
|
76
|
+
let names = Set(listed.map { $0.filename })
|
|
77
|
+
XCTAssertEqual(names, ["a.gguf", "b.gguf"])
|
|
78
|
+
XCTAssertEqual(listed.count, 2)
|
|
79
|
+
// Bytes + sha255 are populated.
|
|
80
|
+
for info in listed {
|
|
81
|
+
XCTAssertGreaterThan(info.bytes, 0)
|
|
82
|
+
XCTAssertEqual(info.sha256.count, 64)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try await downloader.deleteCachedModel(filename: "a.gguf")
|
|
86
|
+
let listed2 = try await downloader.listCachedModels()
|
|
87
|
+
XCTAssertEqual(Set(listed2.map { $0.filename }), ["b.gguf"])
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import DVAILlamaCore
|
|
3
|
-
|
|
4
|
-
final class PluginStateTest: XCTestCase {
|
|
5
|
-
func testStartFailsWhenModelPathMissing() async {
|
|
6
|
-
let state = PluginState()
|
|
7
|
-
do {
|
|
8
|
-
_ = try await state.start(opts: [:])
|
|
9
|
-
XCTFail("should have thrown")
|
|
10
|
-
} catch let error as NSError {
|
|
11
|
-
XCTAssertTrue(error.localizedDescription.contains("modelPath is required"))
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
func testStartFailsWhenModelPathEmpty() async {
|
|
16
|
-
let state = PluginState()
|
|
17
|
-
do {
|
|
18
|
-
_ = try await state.start(opts: ["modelPath": ""])
|
|
19
|
-
XCTFail("should have thrown")
|
|
20
|
-
} catch {
|
|
21
|
-
// expected
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func testStatusInfoReportsNotRunning() async {
|
|
26
|
-
let state = PluginState()
|
|
27
|
-
let info = await state.statusInfo()
|
|
28
|
-
XCTAssertEqual(info["running"] as? Bool, false)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/// With the real LlamaCppBridge implementation, loading a non-existent GGUF
|
|
32
|
-
/// fails at `llama_load_model_from_file`. The full `start → server-bind →
|
|
33
|
-
/// success` happy-path needs a real model file and is exercised by the
|
|
34
|
-
/// device-level tests in Task 37's milestone. Here we assert that the
|
|
35
|
-
/// failure surfaces cleanly and the state stays "not running".
|
|
36
|
-
func testStartFailsOnFakeModelPath() async {
|
|
37
|
-
let state = PluginState()
|
|
38
|
-
do {
|
|
39
|
-
_ = try await state.start(opts: [
|
|
40
|
-
"modelPath": "/tmp/definitely-does-not-exist.gguf",
|
|
41
|
-
"httpBasePort": 39200,
|
|
42
|
-
"httpMaxPortAttempts": 4,
|
|
43
|
-
])
|
|
44
|
-
XCTFail("expected start() to throw for fake model path")
|
|
45
|
-
} catch {
|
|
46
|
-
// expected
|
|
47
|
-
}
|
|
48
|
-
let info = await state.statusInfo()
|
|
49
|
-
XCTAssertEqual(info["running"] as? Bool, false)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import DVAILlamaCore
|
|
3
|
+
|
|
4
|
+
final class PluginStateTest: XCTestCase {
|
|
5
|
+
func testStartFailsWhenModelPathMissing() async {
|
|
6
|
+
let state = PluginState()
|
|
7
|
+
do {
|
|
8
|
+
_ = try await state.start(opts: [:])
|
|
9
|
+
XCTFail("should have thrown")
|
|
10
|
+
} catch let error as NSError {
|
|
11
|
+
XCTAssertTrue(error.localizedDescription.contains("modelPath is required"))
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func testStartFailsWhenModelPathEmpty() async {
|
|
16
|
+
let state = PluginState()
|
|
17
|
+
do {
|
|
18
|
+
_ = try await state.start(opts: ["modelPath": ""])
|
|
19
|
+
XCTFail("should have thrown")
|
|
20
|
+
} catch {
|
|
21
|
+
// expected
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testStatusInfoReportsNotRunning() async {
|
|
26
|
+
let state = PluginState()
|
|
27
|
+
let info = await state.statusInfo()
|
|
28
|
+
XCTAssertEqual(info["running"] as? Bool, false)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// With the real LlamaCppBridge implementation, loading a non-existent GGUF
|
|
32
|
+
/// fails at `llama_load_model_from_file`. The full `start → server-bind →
|
|
33
|
+
/// success` happy-path needs a real model file and is exercised by the
|
|
34
|
+
/// device-level tests in Task 37's milestone. Here we assert that the
|
|
35
|
+
/// failure surfaces cleanly and the state stays "not running".
|
|
36
|
+
func testStartFailsOnFakeModelPath() async {
|
|
37
|
+
let state = PluginState()
|
|
38
|
+
do {
|
|
39
|
+
_ = try await state.start(opts: [
|
|
40
|
+
"modelPath": "/tmp/definitely-does-not-exist.gguf",
|
|
41
|
+
"httpBasePort": 39200,
|
|
42
|
+
"httpMaxPortAttempts": 4,
|
|
43
|
+
])
|
|
44
|
+
XCTFail("expected start() to throw for fake model path")
|
|
45
|
+
} catch {
|
|
46
|
+
// expected
|
|
47
|
+
}
|
|
48
|
+
let info = await state.statusInfo()
|
|
49
|
+
XCTAssertEqual(info["running"] as? Bool, false)
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dvai-bridge/ios-llama-core",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "DVAI-Bridge iOS llama.cpp core — pure Swift / ObjC++ embedded HTTP server + handlers + bridge. Capacitor-free.",
|
|
5
5
|
"author": "Deep Chakraborty <https://github.com/dk013>",
|
|
6
6
|
"license": "Custom (See LICENSE)",
|
|
7
7
|
"main": "Package.swift",
|
|
8
8
|
"files": [
|
|
9
|
-
"
|
|
10
|
-
"ios",
|
|
9
|
+
"ios/Sources",
|
|
10
|
+
"ios/Tests",
|
|
11
11
|
"README.md",
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
package/README.md
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
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.
|