expo-local-llm 0.1.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 +21 -0
- package/README.md +110 -0
- package/android/build.gradle +36 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/localllm/ConversationHistory.kt +39 -0
- package/android/src/main/java/expo/modules/localllm/Enums.kt +13 -0
- package/android/src/main/java/expo/modules/localllm/Exceptions.kt +12 -0
- package/android/src/main/java/expo/modules/localllm/ExpoLocalLlmModule.kt +57 -0
- package/android/src/main/java/expo/modules/localllm/LLMSession.kt +119 -0
- package/android/src/main/java/expo/modules/localllm/Records.kt +23 -0
- package/build/ExpoLocalLlm.types.d.ts +35 -0
- package/build/ExpoLocalLlm.types.d.ts.map +1 -0
- package/build/ExpoLocalLlm.types.js +2 -0
- package/build/ExpoLocalLlm.types.js.map +1 -0
- package/build/ExpoLocalLlmModule.d.ts +10 -0
- package/build/ExpoLocalLlmModule.d.ts.map +1 -0
- package/build/ExpoLocalLlmModule.js +10 -0
- package/build/ExpoLocalLlmModule.js.map +1 -0
- package/build/LLMSession.d.ts +9 -0
- package/build/LLMSession.d.ts.map +1 -0
- package/build/LLMSession.js +16 -0
- package/build/LLMSession.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/useLocalLLM.d.ts +18 -0
- package/build/useLocalLLM.d.ts.map +1 -0
- package/build/useLocalLLM.js +121 -0
- package/build/useLocalLLM.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Enums.swift +11 -0
- package/ios/Exceptions.swift +19 -0
- package/ios/ExpoLocalLlm.podspec +26 -0
- package/ios/ExpoLocalLlmModule.swift +56 -0
- package/ios/FoundationModelBridge.swift +59 -0
- package/ios/LLMSession.swift +68 -0
- package/ios/Records.swift +12 -0
- package/package.json +61 -0
- package/src/ExpoLocalLlm.types.ts +47 -0
- package/src/ExpoLocalLlmModule.ts +19 -0
- package/src/LLMSession.ts +19 -0
- package/src/__tests__/ExpoLocalLlm.test.ts +82 -0
- package/src/__tests__/useLocalLLM.test.ts +30 -0
- package/src/index.ts +4 -0
- package/src/useLocalLLM.ts +176 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PulseID
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# expo-local-llm
|
|
2
|
+
|
|
3
|
+
Expo module for on-device LLM inference. Wraps Apple Foundation Models (iOS 26+) and Gemini Nano via ML Kit (Android).
|
|
4
|
+
|
|
5
|
+
## Platform Requirements
|
|
6
|
+
|
|
7
|
+
| Platform | Requirement |
|
|
8
|
+
|----------|-------------|
|
|
9
|
+
| iOS | iOS 26+ with Apple Intelligence enabled. Loads on iOS 16+ without crashing (returns `notEligible`). |
|
|
10
|
+
| Android | Device with Gemini Nano support (Pixel 8+, Galaxy S25+). Model may require download. |
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install expo-local-llm
|
|
16
|
+
npx expo prebuild
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { useLocalLLM } from 'expo-local-llm';
|
|
23
|
+
|
|
24
|
+
function Chat() {
|
|
25
|
+
const {
|
|
26
|
+
availability,
|
|
27
|
+
isGenerating,
|
|
28
|
+
streamedText,
|
|
29
|
+
error,
|
|
30
|
+
respond,
|
|
31
|
+
streamResponse,
|
|
32
|
+
cancelStream,
|
|
33
|
+
downloadModel,
|
|
34
|
+
downloadProgress,
|
|
35
|
+
} = useLocalLLM({
|
|
36
|
+
instructions: 'You are a helpful assistant.',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Methods are undefined when module is unavailable (optionality pattern)
|
|
40
|
+
if (!streamResponse) {
|
|
41
|
+
return <Text>On-device LLM not available</Text>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleSend = async (prompt: string) => {
|
|
45
|
+
// Streaming — updates streamedText live
|
|
46
|
+
await streamResponse(prompt);
|
|
47
|
+
|
|
48
|
+
// Or non-streaming:
|
|
49
|
+
// const response = await respond(prompt);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View>
|
|
54
|
+
<Text>Status: {availability}</Text>
|
|
55
|
+
{isGenerating && <Text>{streamedText}</Text>}
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
### `useLocalLLM(options?)`
|
|
64
|
+
|
|
65
|
+
#### Options
|
|
66
|
+
|
|
67
|
+
| Option | Type | Description |
|
|
68
|
+
|--------|------|-------------|
|
|
69
|
+
| `instructions` | `string` | System instructions for the session |
|
|
70
|
+
| `options.temperature` | `number` | Sampling temperature |
|
|
71
|
+
| `options.maxTokens` | `number` | Max output tokens (capped at 256 on Android) |
|
|
72
|
+
| `options.topK` | `number` | Top-K sampling |
|
|
73
|
+
|
|
74
|
+
#### Returns
|
|
75
|
+
|
|
76
|
+
| Property | Type | Description |
|
|
77
|
+
|----------|------|-------------|
|
|
78
|
+
| `availability` | `ModelAvailability` | Current model status |
|
|
79
|
+
| `isGenerating` | `boolean` | Whether a generation is in progress |
|
|
80
|
+
| `streamedText` | `string` | Accumulated text from current stream |
|
|
81
|
+
| `downloadProgress` | `number \| null` | Download progress 0-1 (Android only) |
|
|
82
|
+
| `error` | `string \| null` | Last error message |
|
|
83
|
+
| `respond` | `(prompt: string) => Promise<string>` | Non-streaming generation. `undefined` when unavailable. |
|
|
84
|
+
| `streamResponse` | `(prompt: string) => Promise<void>` | Streaming generation (updates `streamedText`). `undefined` when unavailable. |
|
|
85
|
+
| `cancelStream` | `() => Promise<void>` | Cancel active stream. `undefined` when unavailable. |
|
|
86
|
+
| `downloadModel` | `() => Promise<void>` | Trigger model download (Android). `undefined` when unavailable. |
|
|
87
|
+
|
|
88
|
+
### `ModelAvailability`
|
|
89
|
+
|
|
90
|
+
`'available' | 'notEnabled' | 'notReady' | 'notEligible' | 'downloadRequired' | 'downloading' | 'unknown'`
|
|
91
|
+
|
|
92
|
+
## Platform Asymmetries
|
|
93
|
+
|
|
94
|
+
| Concern | iOS | Android |
|
|
95
|
+
|---------|-----|---------|
|
|
96
|
+
| System instructions | Native `LanguageModelSession(instructions:)` | Prepended in prompt text |
|
|
97
|
+
| Output tokens | ~4K context, generous output | 256 max |
|
|
98
|
+
| Model availability | Built-in to OS | May need download |
|
|
99
|
+
| Session history | Native session maintains it | `ConversationHistory` class |
|
|
100
|
+
| Streaming | `AsyncSequence` | Kotlin `Flow` |
|
|
101
|
+
|
|
102
|
+
## Known Limitations
|
|
103
|
+
|
|
104
|
+
- **v0.1.0**: Only one active stream at a time. Events are not scoped to session ID.
|
|
105
|
+
- **Android**: Gemini Nano SDK is in beta. API surface may change.
|
|
106
|
+
- No tool calling or structured output yet.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
id 'org.jetbrains.kotlin.android'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
group = 'expo.modules.localllm'
|
|
8
|
+
version = '0.1.0'
|
|
9
|
+
|
|
10
|
+
android {
|
|
11
|
+
namespace "expo.modules.localllm"
|
|
12
|
+
compileSdk 35
|
|
13
|
+
|
|
14
|
+
defaultConfig {
|
|
15
|
+
minSdk 26
|
|
16
|
+
versionCode 1
|
|
17
|
+
versionName "0.1.0"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
compileOptions {
|
|
21
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
22
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
kotlinOptions {
|
|
26
|
+
jvmTarget = '17'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
lintOptions {
|
|
30
|
+
abortOnError false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dependencies {
|
|
35
|
+
implementation 'com.google.ai.edge.localagent:localagent:0.1.0'
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package expo.modules.localllm
|
|
2
|
+
|
|
3
|
+
class ConversationHistory(private val systemInstructions: String?) {
|
|
4
|
+
private data class Message(val role: String, val content: String)
|
|
5
|
+
|
|
6
|
+
private val messages = mutableListOf<Message>()
|
|
7
|
+
private val maxExchanges = 10
|
|
8
|
+
|
|
9
|
+
fun addUserMessage(content: String) {
|
|
10
|
+
messages.add(Message("User", content))
|
|
11
|
+
trimHistory()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fun addAssistantMessage(content: String) {
|
|
15
|
+
messages.add(Message("Assistant", content))
|
|
16
|
+
trimHistory()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun buildPrompt(): String {
|
|
20
|
+
val sb = StringBuilder()
|
|
21
|
+
if (!systemInstructions.isNullOrBlank()) {
|
|
22
|
+
sb.appendLine("[System] $systemInstructions")
|
|
23
|
+
sb.appendLine()
|
|
24
|
+
}
|
|
25
|
+
for (msg in messages) {
|
|
26
|
+
sb.appendLine("[${msg.role}] ${msg.content}")
|
|
27
|
+
}
|
|
28
|
+
return sb.toString().trim()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private fun trimHistory() {
|
|
32
|
+
// Keep last maxExchanges * 2 messages (user + assistant pairs)
|
|
33
|
+
val maxMessages = maxExchanges * 2
|
|
34
|
+
if (messages.size > maxMessages) {
|
|
35
|
+
val excess = messages.size - maxMessages
|
|
36
|
+
repeat(excess) { messages.removeAt(0) }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package expo.modules.localllm
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.types.Enumerable
|
|
4
|
+
|
|
5
|
+
enum class ModelAvailability(val value: String) : Enumerable {
|
|
6
|
+
available("available"),
|
|
7
|
+
notEnabled("notEnabled"),
|
|
8
|
+
notReady("notReady"),
|
|
9
|
+
notEligible("notEligible"),
|
|
10
|
+
downloadRequired("downloadRequired"),
|
|
11
|
+
downloading("downloading"),
|
|
12
|
+
unknown("unknown")
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package expo.modules.localllm
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.exception.CodedException
|
|
4
|
+
|
|
5
|
+
class NotSupportedException :
|
|
6
|
+
CodedException("ERR_NOT_SUPPORTED", "On-device LLM is not available on this device", null)
|
|
7
|
+
|
|
8
|
+
class SessionInvalidException :
|
|
9
|
+
CodedException("ERR_SESSION_INVALID", "LLM session is invalid or has been destroyed", null)
|
|
10
|
+
|
|
11
|
+
class StreamException(message: String) :
|
|
12
|
+
CodedException("ERR_STREAM", message, null)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package expo.modules.localllm
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
import kotlinx.coroutines.CoroutineScope
|
|
6
|
+
import kotlinx.coroutines.Dispatchers
|
|
7
|
+
import kotlinx.coroutines.launch
|
|
8
|
+
|
|
9
|
+
class ExpoLocalLlmModule : Module() {
|
|
10
|
+
private val scope = CoroutineScope(Dispatchers.Main)
|
|
11
|
+
|
|
12
|
+
override fun definition() = ModuleDefinition {
|
|
13
|
+
Name("ExpoLocalLlm")
|
|
14
|
+
|
|
15
|
+
Events("downloadProgress", "availabilityChange")
|
|
16
|
+
|
|
17
|
+
Function("getAvailability") {
|
|
18
|
+
LLMSession.checkAvailability(appContext.reactContext!!).value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
AsyncFunction("downloadModel") Coroutine { ->
|
|
22
|
+
val context = appContext.reactContext!!
|
|
23
|
+
// Immediately signal downloading state so UI can disable the download button
|
|
24
|
+
sendEvent("availabilityChange", mapOf("availability" to ModelAvailability.downloading.value))
|
|
25
|
+
LLMSession.downloadModel(context) { progress ->
|
|
26
|
+
sendEvent("downloadProgress", mapOf("progress" to progress))
|
|
27
|
+
}
|
|
28
|
+
// Re-check availability after download completes
|
|
29
|
+
val newAvailability = LLMSession.checkAvailability(context)
|
|
30
|
+
sendEvent("availabilityChange", mapOf("availability" to newAvailability.value))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Class(LLMSession::class) {
|
|
34
|
+
Constructor { config: SessionConfig ->
|
|
35
|
+
LLMSession(appContext.reactContext!!, config)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
AsyncFunction("respond") Coroutine { session: LLMSession, prompt: String ->
|
|
39
|
+
session.respond(prompt)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
AsyncFunction("streamResponse") { session: LLMSession, prompt: String ->
|
|
43
|
+
session.startStream(prompt)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
AsyncFunction("cancelStream") { session: LLMSession ->
|
|
47
|
+
session.cancelStream()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Events("token", "streamComplete", "streamError")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
OnActivityDestroys {
|
|
54
|
+
// Cleanup handled by SharedObject.deallocate() on each LLMSession
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package expo.modules.localllm
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.google.ai.edge.localagent.GenerativeModel
|
|
5
|
+
import com.google.ai.edge.localagent.GenerationConfig
|
|
6
|
+
import com.google.ai.edge.localagent.LocalAgent
|
|
7
|
+
import expo.modules.kotlin.sharedobjects.SharedObject
|
|
8
|
+
import kotlinx.coroutines.CancellationException
|
|
9
|
+
import kotlinx.coroutines.CoroutineScope
|
|
10
|
+
import kotlinx.coroutines.Dispatchers
|
|
11
|
+
import kotlinx.coroutines.Job
|
|
12
|
+
import kotlinx.coroutines.cancel
|
|
13
|
+
import kotlinx.coroutines.launch
|
|
14
|
+
|
|
15
|
+
class LLMSession(
|
|
16
|
+
private val context: Context,
|
|
17
|
+
config: SessionConfig
|
|
18
|
+
) : SharedObject() {
|
|
19
|
+
private val history = ConversationHistory(config.instructions)
|
|
20
|
+
private val temperature = config.options?.temperature
|
|
21
|
+
private val maxTokens = config.options?.maxTokens
|
|
22
|
+
private val topK = config.options?.topK
|
|
23
|
+
private var model: GenerativeModel? = null
|
|
24
|
+
private var streamJob: Job? = null
|
|
25
|
+
private val scope = CoroutineScope(Dispatchers.Main)
|
|
26
|
+
|
|
27
|
+
companion object {
|
|
28
|
+
fun checkAvailability(context: Context): ModelAvailability {
|
|
29
|
+
return try {
|
|
30
|
+
val status = LocalAgent.getFeatureStatus(context)
|
|
31
|
+
when {
|
|
32
|
+
status.isAvailable -> ModelAvailability.available
|
|
33
|
+
status.isDownloading -> ModelAvailability.downloading
|
|
34
|
+
status.isDownloadRequired -> ModelAvailability.downloadRequired
|
|
35
|
+
else -> ModelAvailability.notEligible
|
|
36
|
+
}
|
|
37
|
+
} catch (e: Exception) {
|
|
38
|
+
ModelAvailability.notEligible
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
suspend fun downloadModel(context: Context, onProgress: (Float) -> Unit) {
|
|
43
|
+
try {
|
|
44
|
+
// TODO: ML Kit GenAI download API may provide progress callbacks
|
|
45
|
+
// once the SDK stabilizes. For now, report start/complete.
|
|
46
|
+
onProgress(0f)
|
|
47
|
+
LocalAgent.downloadModel(context)
|
|
48
|
+
onProgress(1f)
|
|
49
|
+
} catch (e: Exception) {
|
|
50
|
+
throw StreamException("Failed to download model: ${e.message}")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private suspend fun getOrCreateModel(): GenerativeModel {
|
|
56
|
+
if (model == null) {
|
|
57
|
+
val configBuilder = GenerationConfig.Builder()
|
|
58
|
+
temperature?.let { configBuilder.temperature = it }
|
|
59
|
+
maxTokens?.let { configBuilder.maxOutputTokens = it.coerceAtMost(256) }
|
|
60
|
+
topK?.let { configBuilder.topK = it }
|
|
61
|
+
|
|
62
|
+
model = LocalAgent.createGenerativeModel(
|
|
63
|
+
context = context,
|
|
64
|
+
generationConfig = configBuilder.build()
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
return model!!
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
suspend fun respond(prompt: String): String {
|
|
71
|
+
val genModel = getOrCreateModel()
|
|
72
|
+
history.addUserMessage(prompt)
|
|
73
|
+
val fullPrompt = history.buildPrompt()
|
|
74
|
+
val response = genModel.generateContent(fullPrompt)
|
|
75
|
+
val text = response.text ?: ""
|
|
76
|
+
history.addAssistantMessage(text)
|
|
77
|
+
return text
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fun startStream(prompt: String) {
|
|
81
|
+
streamJob?.cancel()
|
|
82
|
+
history.addUserMessage(prompt)
|
|
83
|
+
val fullPrompt = history.buildPrompt()
|
|
84
|
+
|
|
85
|
+
streamJob = scope.launch {
|
|
86
|
+
try {
|
|
87
|
+
val genModel = getOrCreateModel()
|
|
88
|
+
var accumulated = ""
|
|
89
|
+
val responseStream = genModel.generateContentStream(fullPrompt)
|
|
90
|
+
responseStream.collect { chunk ->
|
|
91
|
+
val token = chunk.text ?: ""
|
|
92
|
+
accumulated += token
|
|
93
|
+
emit("token", mapOf(
|
|
94
|
+
"token" to token,
|
|
95
|
+
"accumulated" to accumulated
|
|
96
|
+
))
|
|
97
|
+
}
|
|
98
|
+
history.addAssistantMessage(accumulated)
|
|
99
|
+
emit("streamComplete", mapOf("text" to accumulated))
|
|
100
|
+
} catch (e: CancellationException) {
|
|
101
|
+
// User-initiated cancellation — not an error
|
|
102
|
+
} catch (e: Exception) {
|
|
103
|
+
emit("streamError", mapOf("error" to (e.message ?: "Unknown error")))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fun cancelStream() {
|
|
109
|
+
streamJob?.cancel()
|
|
110
|
+
streamJob = null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override fun deallocate() {
|
|
114
|
+
super.deallocate()
|
|
115
|
+
streamJob?.cancel()
|
|
116
|
+
model = null
|
|
117
|
+
scope.cancel()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package expo.modules.localllm
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.records.Field
|
|
4
|
+
import expo.modules.kotlin.records.Record
|
|
5
|
+
|
|
6
|
+
class SessionConfig : Record {
|
|
7
|
+
@Field
|
|
8
|
+
var instructions: String? = null
|
|
9
|
+
|
|
10
|
+
@Field
|
|
11
|
+
var options: GenerationOptionsRecord? = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class GenerationOptionsRecord : Record {
|
|
15
|
+
@Field
|
|
16
|
+
var temperature: Float? = null
|
|
17
|
+
|
|
18
|
+
@Field
|
|
19
|
+
var maxTokens: Int? = null
|
|
20
|
+
|
|
21
|
+
@Field
|
|
22
|
+
var topK: Int? = null
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type ModelAvailability = "available" | "notEnabled" | "notReady" | "notEligible" | "downloadRequired" | "downloading" | "unknown";
|
|
2
|
+
export type GenerationOptions = {
|
|
3
|
+
temperature?: number;
|
|
4
|
+
maxTokens?: number;
|
|
5
|
+
topK?: number;
|
|
6
|
+
};
|
|
7
|
+
export type SessionConfig = {
|
|
8
|
+
instructions?: string;
|
|
9
|
+
options?: GenerationOptions;
|
|
10
|
+
};
|
|
11
|
+
export type TokenEvent = {
|
|
12
|
+
token: string;
|
|
13
|
+
accumulated: string;
|
|
14
|
+
};
|
|
15
|
+
export type StreamCompleteEvent = {
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
export type StreamErrorEvent = {
|
|
19
|
+
error: string;
|
|
20
|
+
};
|
|
21
|
+
export type DownloadProgress = {
|
|
22
|
+
progress: number;
|
|
23
|
+
};
|
|
24
|
+
export type LLMSessionEvents = {
|
|
25
|
+
token: (event: TokenEvent) => void;
|
|
26
|
+
streamComplete: (event: StreamCompleteEvent) => void;
|
|
27
|
+
streamError: (event: StreamErrorEvent) => void;
|
|
28
|
+
};
|
|
29
|
+
export type ExpoLocalLlmModuleEvents = {
|
|
30
|
+
downloadProgress: (event: DownloadProgress) => void;
|
|
31
|
+
availabilityChange: (event: {
|
|
32
|
+
availability: ModelAvailability;
|
|
33
|
+
}) => void;
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=ExpoLocalLlm.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoLocalLlm.types.d.ts","sourceRoot":"","sources":["../src/ExpoLocalLlm.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,YAAY,GACZ,UAAU,GACV,aAAa,GACb,kBAAkB,GAClB,aAAa,GACb,SAAS,CAAC;AAEd,MAAM,MAAM,iBAAiB,GAAG;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACnC,cAAc,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACrD,WAAW,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,gBAAgB,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACpD,kBAAkB,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,iBAAiB,CAAA;KAAE,KAAK,IAAI,CAAC;CAC1E,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoLocalLlm.types.js","sourceRoot":"","sources":["../src/ExpoLocalLlm.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ModelAvailability =\n | \"available\"\n | \"notEnabled\"\n | \"notReady\"\n | \"notEligible\"\n | \"downloadRequired\"\n | \"downloading\"\n | \"unknown\";\n\nexport type GenerationOptions = {\n temperature?: number;\n maxTokens?: number;\n topK?: number;\n};\n\nexport type SessionConfig = {\n instructions?: string;\n options?: GenerationOptions;\n};\n\nexport type TokenEvent = {\n token: string;\n accumulated: string;\n};\n\nexport type StreamCompleteEvent = {\n text: string;\n};\n\nexport type StreamErrorEvent = {\n error: string;\n};\n\nexport type DownloadProgress = {\n progress: number;\n};\n\nexport type LLMSessionEvents = {\n token: (event: TokenEvent) => void;\n streamComplete: (event: StreamCompleteEvent) => void;\n streamError: (event: StreamErrorEvent) => void;\n};\n\nexport type ExpoLocalLlmModuleEvents = {\n downloadProgress: (event: DownloadProgress) => void;\n availabilityChange: (event: { availability: ModelAvailability }) => void;\n};\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NativeModule } from "expo-modules-core";
|
|
2
|
+
import type { ExpoLocalLlmModuleEvents } from "./ExpoLocalLlm.types";
|
|
3
|
+
declare class ExpoLocalLlmModuleType extends NativeModule<ExpoLocalLlmModuleEvents> {
|
|
4
|
+
getAvailability(): string;
|
|
5
|
+
downloadModel(): Promise<void>;
|
|
6
|
+
LLMSession: any;
|
|
7
|
+
}
|
|
8
|
+
declare let nativeModule: ExpoLocalLlmModuleType | null;
|
|
9
|
+
export default nativeModule;
|
|
10
|
+
//# sourceMappingURL=ExpoLocalLlmModule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoLocalLlmModule.d.ts","sourceRoot":"","sources":["../src/ExpoLocalLlmModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAErE,OAAO,OAAO,sBAAuB,SAAQ,YAAY,CAAC,wBAAwB,CAAC;IACjF,eAAe,IAAI,MAAM;IACzB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAC9B,UAAU,EAAE,GAAG,CAAC;CACjB;AAED,QAAA,IAAI,YAAY,EAAE,sBAAsB,GAAG,IAAI,CAAC;AAQhD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoLocalLlmModule.js","sourceRoot":"","sources":["../src/ExpoLocalLlmModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAUtE,IAAI,YAA2C,CAAC;AAEhD,IAAI,CAAC;IACH,YAAY,GAAG,mBAAmB,CAAyB,cAAc,CAAC,CAAC;AAC7E,CAAC;AAAC,MAAM,CAAC;IACP,YAAY,GAAG,IAAI,CAAC;AACtB,CAAC;AAED,eAAe,YAAY,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo-modules-core\";\n\nimport type { ExpoLocalLlmModuleEvents } from \"./ExpoLocalLlm.types\";\n\ndeclare class ExpoLocalLlmModuleType extends NativeModule<ExpoLocalLlmModuleEvents> {\n getAvailability(): string;\n downloadModel(): Promise<void>;\n LLMSession: any;\n}\n\nlet nativeModule: ExpoLocalLlmModuleType | null;\n\ntry {\n nativeModule = requireNativeModule<ExpoLocalLlmModuleType>(\"ExpoLocalLlm\");\n} catch {\n nativeModule = null;\n}\n\nexport default nativeModule;\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SharedObject } from "expo-modules-core";
|
|
2
|
+
import type { SessionConfig, LLMSessionEvents } from "./ExpoLocalLlm.types";
|
|
3
|
+
export declare class LLMSession extends SharedObject<LLMSessionEvents> {
|
|
4
|
+
respond: (prompt: string) => Promise<string>;
|
|
5
|
+
streamResponse: (prompt: string) => Promise<void>;
|
|
6
|
+
cancelStream: () => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createLLMSession(config?: SessionConfig): LLMSession;
|
|
9
|
+
//# sourceMappingURL=LLMSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LLMSession.d.ts","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG5E,qBAAa,UAAW,SAAQ,YAAY,CAAC,gBAAgB,CAAC;IAG5D,OAAO,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,EAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAKvE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SharedObject, UnavailabilityError } from "expo-modules-core";
|
|
2
|
+
import ExpoLocalLlmModule from "./ExpoLocalLlmModule";
|
|
3
|
+
export class LLMSession extends SharedObject {
|
|
4
|
+
// Methods are implemented natively via the Class() DSL.
|
|
5
|
+
// TypeScript declarations provide type safety only.
|
|
6
|
+
respond;
|
|
7
|
+
streamResponse;
|
|
8
|
+
cancelStream;
|
|
9
|
+
}
|
|
10
|
+
export function createLLMSession(config = {}) {
|
|
11
|
+
if (!ExpoLocalLlmModule) {
|
|
12
|
+
throw new UnavailabilityError("ExpoLocalLlm", "createLLMSession");
|
|
13
|
+
}
|
|
14
|
+
return new ExpoLocalLlmModule.LLMSession(config);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=LLMSession.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LLMSession.js","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGtE,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AAEtD,MAAM,OAAO,UAAW,SAAQ,YAA8B;IAC5D,wDAAwD;IACxD,oDAAoD;IACpD,OAAO,CAAuC;IAC9C,cAAc,CAAqC;IACnD,YAAY,CAAuB;CACpC;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAwB,EAAE;IACzD,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,MAAM,IAAI,mBAAmB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,IAAI,kBAAkB,CAAC,UAAU,CAAC,MAAM,CAAe,CAAC;AACjE,CAAC","sourcesContent":["import { SharedObject, UnavailabilityError } from \"expo-modules-core\";\n\nimport type { SessionConfig, LLMSessionEvents } from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\n\nexport class LLMSession extends SharedObject<LLMSessionEvents> {\n // Methods are implemented natively via the Class() DSL.\n // TypeScript declarations provide type safety only.\n respond!: (prompt: string) => Promise<string>;\n streamResponse!: (prompt: string) => Promise<void>;\n cancelStream!: () => Promise<void>;\n}\n\nexport function createLLMSession(config: SessionConfig = {}): LLMSession {\n if (!ExpoLocalLlmModule) {\n throw new UnavailabilityError(\"ExpoLocalLlm\", \"createLLMSession\");\n }\n return new ExpoLocalLlmModule.LLMSession(config) as LLMSession;\n}\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,cAAc,sBAAsB,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,cAAc,sBAAsB,CAAC","sourcesContent":["export { useLocalLLM } from \"./useLocalLLM\";\nexport { LLMSession, createLLMSession } from \"./LLMSession\";\nexport { default as ExpoLocalLlmModule } from \"./ExpoLocalLlmModule\";\nexport * from \"./ExpoLocalLlm.types\";\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ModelAvailability, SessionConfig } from "./ExpoLocalLlm.types";
|
|
2
|
+
import { LLMSession } from "./LLMSession";
|
|
3
|
+
type UseLocalLLMOptions = SessionConfig;
|
|
4
|
+
type UseLocalLLMResult = {
|
|
5
|
+
availability: ModelAvailability;
|
|
6
|
+
session: LLMSession | null;
|
|
7
|
+
isGenerating: boolean;
|
|
8
|
+
streamedText: string;
|
|
9
|
+
downloadProgress: number | null;
|
|
10
|
+
error: string | null;
|
|
11
|
+
respond?: (prompt: string) => Promise<string>;
|
|
12
|
+
streamResponse?: (prompt: string) => Promise<void>;
|
|
13
|
+
cancelStream?: () => Promise<void>;
|
|
14
|
+
downloadModel?: () => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare function useLocalLLM(options?: UseLocalLLMOptions): UseLocalLLMResult;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=useLocalLLM.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLocalLLM.d.ts","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EAKd,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAoB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,KAAK,kBAAkB,GAAG,aAAa,CAAC;AAExC,KAAK,iBAAiB,GAAG;IACvB,YAAY,EAAE,iBAAiB,CAAC;IAChC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;AAEF,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAiJnB"}
|