@dvai-bridge/ios-llama-core 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/LICENSE +51 -0
- package/Package.swift +71 -0
- package/README.md +199 -0
- package/ios/Sources/DVAILlamaCore/AudioDecoder.swift +112 -0
- package/ios/Sources/DVAILlamaCore/ContentPartsTranslator.swift +232 -0
- package/ios/Sources/DVAILlamaCore/ImageDecoder.swift +91 -0
- package/ios/Sources/DVAILlamaCore/LlamaCppBridgeProtocol.swift +59 -0
- package/ios/Sources/DVAILlamaCore/LlamaHandlers.swift +422 -0
- package/ios/Sources/DVAILlamaCore/ModelDownloader.swift +445 -0
- package/ios/Sources/DVAILlamaCore/PluginState.swift +158 -0
- package/ios/Sources/DVAILlamaCoreObjC/LlamaCppBridge.mm +649 -0
- package/ios/Sources/DVAILlamaCoreObjC/include/LlamaCppBridge.h +101 -0
- package/ios/Tests/DVAILlamaCoreTests/AudioDecoderTest.swift +46 -0
- package/ios/Tests/DVAILlamaCoreTests/ContentPartsTranslatorTest.swift +361 -0
- package/ios/Tests/DVAILlamaCoreTests/ImageDecoderTest.swift +139 -0
- package/ios/Tests/DVAILlamaCoreTests/LlamaCppBridgeTest.swift +131 -0
- package/ios/Tests/DVAILlamaCoreTests/LlamaHandlersTest.swift +515 -0
- package/ios/Tests/DVAILlamaCoreTests/ModelDownloaderTest.swift +89 -0
- package/ios/Tests/DVAILlamaCoreTests/PluginStateTest.swift +51 -0
- package/package.json +18 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dvai-bridge/ios-llama-core",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "DVAI-Bridge iOS llama.cpp core — pure Swift / ObjC++ embedded HTTP server + handlers + bridge. Capacitor-free.",
|
|
5
|
+
"author": "Deep Chakraborty <https://github.com/dk013>",
|
|
6
|
+
"license": "Custom (See LICENSE)",
|
|
7
|
+
"main": "Package.swift",
|
|
8
|
+
"files": [
|
|
9
|
+
"Package.swift",
|
|
10
|
+
"ios",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"registry": "https://registry.npmjs.org/",
|
|
16
|
+
"access": "public"
|
|
17
|
+
}
|
|
18
|
+
}
|