@elizaos/capacitor-llama 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/README.md +68 -0
- package/dist/esm/capacitor-llama-adapter.d.ts +5 -0
- package/dist/esm/capacitor-llama-adapter.js +262 -0
- package/dist/esm/definitions.d.ts +92 -0
- package/dist/esm/definitions.js +10 -0
- package/dist/esm/device-bridge-client.d.ts +48 -0
- package/dist/esm/device-bridge-client.js +221 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.test.d.ts +1 -0
- package/dist/esm/index.test.js +264 -0
- package/dist/esm/load-capacitor-llama.d.ts +2 -0
- package/dist/esm/load-capacitor-llama.js +9 -0
- package/dist/esm/web.d.ts +11 -0
- package/dist/esm/web.js +10 -0
- package/dist/plugin.cjs.js +500 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +505 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-side half of the agent↔device inference bridge.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the mobile app (Capacitor iOS / Android) and dials out to
|
|
5
|
+
* the agent container over WebSocket. Receives `generate` requests,
|
|
6
|
+
* forwards to `capacitorLlama`, returns results. Auto-reconnects with
|
|
7
|
+
* exponential backoff when the link drops.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the message envelope defined in
|
|
10
|
+
* `@elizaos/app-core/src/services/local-inference/device-bridge.ts`.
|
|
11
|
+
* Keep the two in sync by hand — the message shape is the bridge
|
|
12
|
+
* contract.
|
|
13
|
+
*/
|
|
14
|
+
import { loadCapacitorLlama } from "./load-capacitor-llama";
|
|
15
|
+
const INITIAL_BACKOFF_MS = 1000;
|
|
16
|
+
const MAX_BACKOFF_MS = 30000;
|
|
17
|
+
export class DeviceBridgeClient {
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.socket = null;
|
|
20
|
+
this.reconnectAttempt = 0;
|
|
21
|
+
this.stopped = false;
|
|
22
|
+
this.config = config;
|
|
23
|
+
}
|
|
24
|
+
start() {
|
|
25
|
+
this.stopped = false;
|
|
26
|
+
this.connect();
|
|
27
|
+
}
|
|
28
|
+
stop() {
|
|
29
|
+
this.stopped = true;
|
|
30
|
+
if (this.socket) {
|
|
31
|
+
try {
|
|
32
|
+
this.socket.close(1000, "client-stop");
|
|
33
|
+
}
|
|
34
|
+
catch (_a) {
|
|
35
|
+
/* best effort */
|
|
36
|
+
}
|
|
37
|
+
this.socket = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
computeBackoffMs() {
|
|
41
|
+
const exp = Math.min(MAX_BACKOFF_MS, INITIAL_BACKOFF_MS * 2 ** Math.min(this.reconnectAttempt, 6));
|
|
42
|
+
// Full jitter: uniform random in [0, exp).
|
|
43
|
+
return Math.floor(Math.random() * exp);
|
|
44
|
+
}
|
|
45
|
+
connect() {
|
|
46
|
+
var _a, _b, _c, _d;
|
|
47
|
+
if (this.stopped)
|
|
48
|
+
return;
|
|
49
|
+
(_b = (_a = this.config).onStateChange) === null || _b === void 0 ? void 0 : _b.call(_a, "connecting");
|
|
50
|
+
const url = this.buildUrl();
|
|
51
|
+
let ws;
|
|
52
|
+
try {
|
|
53
|
+
ws = new WebSocket(url);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
(_d = (_c = this.config).onStateChange) === null || _d === void 0 ? void 0 : _d.call(_c, "error", err instanceof Error ? err.message : String(err));
|
|
57
|
+
this.scheduleReconnect();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.socket = ws;
|
|
61
|
+
ws.onopen = () => {
|
|
62
|
+
this.reconnectAttempt = 0;
|
|
63
|
+
void this.sendRegister(ws);
|
|
64
|
+
};
|
|
65
|
+
ws.onmessage = (event) => {
|
|
66
|
+
let msg;
|
|
67
|
+
try {
|
|
68
|
+
msg = JSON.parse(String(event.data));
|
|
69
|
+
}
|
|
70
|
+
catch (_a) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
void this.handleAgentMessage(ws, msg);
|
|
74
|
+
};
|
|
75
|
+
ws.onerror = () => {
|
|
76
|
+
var _a, _b;
|
|
77
|
+
(_b = (_a = this.config).onStateChange) === null || _b === void 0 ? void 0 : _b.call(_a, "error", "websocket error");
|
|
78
|
+
};
|
|
79
|
+
ws.onclose = () => {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
this.socket = null;
|
|
82
|
+
(_b = (_a = this.config).onStateChange) === null || _b === void 0 ? void 0 : _b.call(_a, "disconnected");
|
|
83
|
+
this.scheduleReconnect();
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
buildUrl() {
|
|
87
|
+
if (!this.config.pairingToken)
|
|
88
|
+
return this.config.agentUrl;
|
|
89
|
+
const hasQuery = this.config.agentUrl.includes("?");
|
|
90
|
+
const sep = hasQuery ? "&" : "?";
|
|
91
|
+
return `${this.config.agentUrl}${sep}token=${encodeURIComponent(this.config.pairingToken)}`;
|
|
92
|
+
}
|
|
93
|
+
scheduleReconnect() {
|
|
94
|
+
if (this.stopped)
|
|
95
|
+
return;
|
|
96
|
+
const delay = this.computeBackoffMs();
|
|
97
|
+
this.reconnectAttempt += 1;
|
|
98
|
+
setTimeout(() => this.connect(), delay);
|
|
99
|
+
}
|
|
100
|
+
async sendRegister(ws) {
|
|
101
|
+
var _a, _b;
|
|
102
|
+
const capacitorLlama = await loadCapacitorLlama();
|
|
103
|
+
const hardware = await capacitorLlama.getHardwareInfo();
|
|
104
|
+
const loaded = await capacitorLlama.isLoaded();
|
|
105
|
+
const msg = {
|
|
106
|
+
type: "register",
|
|
107
|
+
payload: {
|
|
108
|
+
deviceId: this.config.deviceId,
|
|
109
|
+
pairingToken: this.config.pairingToken,
|
|
110
|
+
capabilities: {
|
|
111
|
+
platform: hardware.platform,
|
|
112
|
+
deviceModel: hardware.deviceModel,
|
|
113
|
+
totalRamGb: hardware.totalRamGb,
|
|
114
|
+
cpuCores: hardware.cpuCores,
|
|
115
|
+
gpu: hardware.gpu,
|
|
116
|
+
},
|
|
117
|
+
loadedPath: loaded.modelPath,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
this.send(ws, msg);
|
|
121
|
+
(_b = (_a = this.config).onStateChange) === null || _b === void 0 ? void 0 : _b.call(_a, "connected");
|
|
122
|
+
}
|
|
123
|
+
send(ws, msg) {
|
|
124
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
125
|
+
return;
|
|
126
|
+
ws.send(JSON.stringify(msg));
|
|
127
|
+
}
|
|
128
|
+
async handleAgentMessage(ws, msg) {
|
|
129
|
+
if (msg.type === "ping") {
|
|
130
|
+
this.send(ws, { type: "pong", at: Date.now() });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (msg.type === "load") {
|
|
134
|
+
try {
|
|
135
|
+
const capacitorLlama = await loadCapacitorLlama();
|
|
136
|
+
await capacitorLlama.load({
|
|
137
|
+
modelPath: msg.modelPath,
|
|
138
|
+
contextSize: msg.contextSize,
|
|
139
|
+
useGpu: msg.useGpu,
|
|
140
|
+
});
|
|
141
|
+
this.send(ws, {
|
|
142
|
+
type: "loadResult",
|
|
143
|
+
correlationId: msg.correlationId,
|
|
144
|
+
ok: true,
|
|
145
|
+
loadedPath: msg.modelPath,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
this.send(ws, {
|
|
150
|
+
type: "loadResult",
|
|
151
|
+
correlationId: msg.correlationId,
|
|
152
|
+
ok: false,
|
|
153
|
+
error: err instanceof Error ? err.message : String(err),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (msg.type === "unload") {
|
|
159
|
+
try {
|
|
160
|
+
const capacitorLlama = await loadCapacitorLlama();
|
|
161
|
+
await capacitorLlama.unload();
|
|
162
|
+
this.send(ws, {
|
|
163
|
+
type: "unloadResult",
|
|
164
|
+
correlationId: msg.correlationId,
|
|
165
|
+
ok: true,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.send(ws, {
|
|
170
|
+
type: "unloadResult",
|
|
171
|
+
correlationId: msg.correlationId,
|
|
172
|
+
ok: false,
|
|
173
|
+
error: err instanceof Error ? err.message : String(err),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (msg.type === "generate") {
|
|
179
|
+
try {
|
|
180
|
+
const capacitorLlama = await loadCapacitorLlama();
|
|
181
|
+
const result = await capacitorLlama.generate({
|
|
182
|
+
prompt: msg.prompt,
|
|
183
|
+
stopSequences: msg.stopSequences,
|
|
184
|
+
maxTokens: msg.maxTokens,
|
|
185
|
+
temperature: msg.temperature,
|
|
186
|
+
});
|
|
187
|
+
this.send(ws, {
|
|
188
|
+
type: "generateResult",
|
|
189
|
+
correlationId: msg.correlationId,
|
|
190
|
+
ok: true,
|
|
191
|
+
text: result.text,
|
|
192
|
+
promptTokens: result.promptTokens,
|
|
193
|
+
outputTokens: result.outputTokens,
|
|
194
|
+
durationMs: result.durationMs,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
this.send(ws, {
|
|
199
|
+
type: "generateResult",
|
|
200
|
+
correlationId: msg.correlationId,
|
|
201
|
+
ok: false,
|
|
202
|
+
error: err instanceof Error ? err.message : String(err),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Convenience helper for the mobile bootstrap: starts a bridge client
|
|
211
|
+
* using values from the Eliza config or hardcoded env.
|
|
212
|
+
*
|
|
213
|
+
* The host app is expected to call this once during Capacitor bootstrap.
|
|
214
|
+
* `agentUrl` and `pairingToken` come from the user's pairing flow and
|
|
215
|
+
* should be persisted across launches.
|
|
216
|
+
*/
|
|
217
|
+
export function startDeviceBridgeClient(config) {
|
|
218
|
+
const client = new DeviceBridgeClient(config);
|
|
219
|
+
client.start();
|
|
220
|
+
return client;
|
|
221
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @elizaos/capacitor-llama
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter that maps `llama-cpp-capacitor`'s contextId-based API onto
|
|
5
|
+
* Eliza's `LocalInferenceLoader` contract. At most one native context lives
|
|
6
|
+
* at a time; switching models disposes the previous context first so we
|
|
7
|
+
* never double-allocate VRAM.
|
|
8
|
+
*
|
|
9
|
+
* On web this package falls back to an "unavailable" stub. Mobile builds
|
|
10
|
+
* should call `registerCapacitorLlamaLoader(runtime)` during bootstrap to
|
|
11
|
+
* wire this adapter in as the runtime's `localInferenceLoader` service.
|
|
12
|
+
*/
|
|
13
|
+
export { capacitorLlama, registerCapacitorLlamaLoader, } from "./capacitor-llama-adapter";
|
|
14
|
+
export * from "./definitions";
|
|
15
|
+
export { DeviceBridgeClient, type DeviceBridgeClientConfig, startDeviceBridgeClient, } from "./device-bridge-client";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @elizaos/capacitor-llama
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter that maps `llama-cpp-capacitor`'s contextId-based API onto
|
|
5
|
+
* Eliza's `LocalInferenceLoader` contract. At most one native context lives
|
|
6
|
+
* at a time; switching models disposes the previous context first so we
|
|
7
|
+
* never double-allocate VRAM.
|
|
8
|
+
*
|
|
9
|
+
* On web this package falls back to an "unavailable" stub. Mobile builds
|
|
10
|
+
* should call `registerCapacitorLlamaLoader(runtime)` during bootstrap to
|
|
11
|
+
* wire this adapter in as the runtime's `localInferenceLoader` service.
|
|
12
|
+
*/
|
|
13
|
+
export { capacitorLlama, registerCapacitorLlamaLoader, } from "./capacitor-llama-adapter";
|
|
14
|
+
export * from "./definitions";
|
|
15
|
+
export { DeviceBridgeClient, startDeviceBridgeClient, } from "./device-bridge-client";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
function setNativeCapacitor(platform = "ios") {
|
|
3
|
+
globalThis.Capacitor = {
|
|
4
|
+
getPlatform: () => platform,
|
|
5
|
+
isNativePlatform: () => true,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function clearNativeCapacitor() {
|
|
9
|
+
delete globalThis.Capacitor;
|
|
10
|
+
}
|
|
11
|
+
function makePluginMock() {
|
|
12
|
+
let tokenListener = null;
|
|
13
|
+
const listenerHandle = { remove: vi.fn(async () => undefined) };
|
|
14
|
+
return {
|
|
15
|
+
initContext: vi.fn(async (_options) => ({
|
|
16
|
+
contextId: 1,
|
|
17
|
+
gpu: true,
|
|
18
|
+
reasonNoGPU: "",
|
|
19
|
+
model: {
|
|
20
|
+
desc: "test",
|
|
21
|
+
size: 1,
|
|
22
|
+
nEmbd: 1,
|
|
23
|
+
nParams: 1,
|
|
24
|
+
chatTemplates: {
|
|
25
|
+
llamaChat: false,
|
|
26
|
+
minja: {
|
|
27
|
+
default: false,
|
|
28
|
+
defaultCaps: {
|
|
29
|
+
tools: false,
|
|
30
|
+
toolCalls: false,
|
|
31
|
+
toolResponses: false,
|
|
32
|
+
systemRole: false,
|
|
33
|
+
parallelToolCalls: false,
|
|
34
|
+
toolCallId: false,
|
|
35
|
+
},
|
|
36
|
+
toolUse: false,
|
|
37
|
+
toolUseCaps: {
|
|
38
|
+
tools: false,
|
|
39
|
+
toolCalls: false,
|
|
40
|
+
toolResponses: false,
|
|
41
|
+
systemRole: false,
|
|
42
|
+
parallelToolCalls: false,
|
|
43
|
+
toolCallId: false,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
metadata: {},
|
|
48
|
+
isChatTemplateSupported: false,
|
|
49
|
+
},
|
|
50
|
+
})),
|
|
51
|
+
releaseContext: vi.fn(async (_options) => undefined),
|
|
52
|
+
releaseAllContexts: vi.fn(async () => undefined),
|
|
53
|
+
generateText: vi.fn(async (_options) => ({
|
|
54
|
+
text: "hello",
|
|
55
|
+
reasoning_content: "",
|
|
56
|
+
tool_calls: [],
|
|
57
|
+
content: "hello",
|
|
58
|
+
chat_format: 0,
|
|
59
|
+
tokens_predicted: 2,
|
|
60
|
+
tokens_evaluated: 3,
|
|
61
|
+
truncated: false,
|
|
62
|
+
stopped_eos: true,
|
|
63
|
+
stopped_word: "",
|
|
64
|
+
stopped_limit: 0,
|
|
65
|
+
stopping_word: "",
|
|
66
|
+
context_full: false,
|
|
67
|
+
interrupted: false,
|
|
68
|
+
tokens_cached: 0,
|
|
69
|
+
timings: {
|
|
70
|
+
prompt_n: 3,
|
|
71
|
+
prompt_ms: 1,
|
|
72
|
+
prompt_per_token_ms: 1,
|
|
73
|
+
prompt_per_second: 1,
|
|
74
|
+
predicted_n: 2,
|
|
75
|
+
predicted_ms: 7,
|
|
76
|
+
predicted_per_token_ms: 1,
|
|
77
|
+
predicted_per_second: 1,
|
|
78
|
+
},
|
|
79
|
+
})),
|
|
80
|
+
stopCompletion: vi.fn(async (_options) => undefined),
|
|
81
|
+
embedding: vi.fn(async (_options) => ({
|
|
82
|
+
embedding: [0.5, -0.25, 0.75],
|
|
83
|
+
})),
|
|
84
|
+
tokenize: vi.fn(async (_options) => ({
|
|
85
|
+
tokens: [1, 2, 3, 4],
|
|
86
|
+
})),
|
|
87
|
+
addListener: vi.fn(async (_event, listener) => {
|
|
88
|
+
tokenListener = listener;
|
|
89
|
+
return listenerHandle;
|
|
90
|
+
}),
|
|
91
|
+
emitToken(token) {
|
|
92
|
+
tokenListener === null || tokenListener === void 0 ? void 0 : tokenListener({ tokenResult: { token } });
|
|
93
|
+
},
|
|
94
|
+
listenerHandle,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
let mockedPlugin = null;
|
|
98
|
+
function currentPlugin() {
|
|
99
|
+
if (!mockedPlugin) {
|
|
100
|
+
throw new Error("llama-cpp-capacitor mock was not configured");
|
|
101
|
+
}
|
|
102
|
+
return mockedPlugin;
|
|
103
|
+
}
|
|
104
|
+
const llamaCppProxy = {
|
|
105
|
+
initContext(options) {
|
|
106
|
+
return currentPlugin().initContext(options);
|
|
107
|
+
},
|
|
108
|
+
releaseContext(options) {
|
|
109
|
+
return currentPlugin().releaseContext(options);
|
|
110
|
+
},
|
|
111
|
+
releaseAllContexts() {
|
|
112
|
+
return currentPlugin().releaseAllContexts();
|
|
113
|
+
},
|
|
114
|
+
generateText(options) {
|
|
115
|
+
return currentPlugin().generateText(options);
|
|
116
|
+
},
|
|
117
|
+
stopCompletion(options) {
|
|
118
|
+
return currentPlugin().stopCompletion(options);
|
|
119
|
+
},
|
|
120
|
+
embedding(options) {
|
|
121
|
+
return currentPlugin().embedding(options);
|
|
122
|
+
},
|
|
123
|
+
tokenize(options) {
|
|
124
|
+
return currentPlugin().tokenize(options);
|
|
125
|
+
},
|
|
126
|
+
addListener(event, listener) {
|
|
127
|
+
return currentPlugin().addListener(event, listener);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
vi.mock("llama-cpp-capacitor", () => ({ LlamaCpp: llamaCppProxy }));
|
|
131
|
+
afterEach(async () => {
|
|
132
|
+
var _a;
|
|
133
|
+
// Reset the module-level `capacitorLlama` singleton between tests so
|
|
134
|
+
// state from a prior load() doesn't leak into the next case.
|
|
135
|
+
try {
|
|
136
|
+
const mod = await import("./index");
|
|
137
|
+
const adapter = mod.capacitorLlama;
|
|
138
|
+
await ((_a = adapter.dispose) === null || _a === void 0 ? void 0 : _a.call(adapter));
|
|
139
|
+
}
|
|
140
|
+
catch (_b) {
|
|
141
|
+
/* dispose is best-effort */
|
|
142
|
+
}
|
|
143
|
+
clearNativeCapacitor();
|
|
144
|
+
mockedPlugin = null;
|
|
145
|
+
});
|
|
146
|
+
describe("@elizaos/capacitor-llama adapter", () => {
|
|
147
|
+
it("rejects model loads outside native Capacitor", async () => {
|
|
148
|
+
const plugin = makePluginMock();
|
|
149
|
+
mockedPlugin = plugin;
|
|
150
|
+
const { capacitorLlama } = await import("./index");
|
|
151
|
+
await expect(capacitorLlama.load({ modelPath: "/models/test.gguf" })).rejects.toThrow("only available on iOS and Android");
|
|
152
|
+
expect(plugin.initContext).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
it("loads, generates, streams tokens, and unloads through llama-cpp-capacitor", async () => {
|
|
155
|
+
setNativeCapacitor("ios");
|
|
156
|
+
const plugin = makePluginMock();
|
|
157
|
+
mockedPlugin = plugin;
|
|
158
|
+
const { capacitorLlama } = await import("./index");
|
|
159
|
+
const tokens = [];
|
|
160
|
+
const offToken = capacitorLlama.onToken((token, index) => {
|
|
161
|
+
tokens.push([token, index]);
|
|
162
|
+
});
|
|
163
|
+
await capacitorLlama.load({
|
|
164
|
+
modelPath: "/models/test.gguf",
|
|
165
|
+
contextSize: 2048,
|
|
166
|
+
useGpu: true,
|
|
167
|
+
maxThreads: 4,
|
|
168
|
+
});
|
|
169
|
+
plugin.emitToken("h");
|
|
170
|
+
const result = await capacitorLlama.generate({
|
|
171
|
+
prompt: "Say hello",
|
|
172
|
+
maxTokens: 16,
|
|
173
|
+
temperature: 0.2,
|
|
174
|
+
topP: 0.8,
|
|
175
|
+
stream: true,
|
|
176
|
+
stopSequences: ["</s>"],
|
|
177
|
+
});
|
|
178
|
+
offToken();
|
|
179
|
+
await capacitorLlama.unload();
|
|
180
|
+
expect(plugin.initContext).toHaveBeenCalledWith({
|
|
181
|
+
contextId: 1,
|
|
182
|
+
params: {
|
|
183
|
+
model: "/models/test.gguf",
|
|
184
|
+
n_ctx: 2048,
|
|
185
|
+
n_gpu_layers: 99,
|
|
186
|
+
n_threads: 4,
|
|
187
|
+
use_mmap: true,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
expect(plugin.generateText).toHaveBeenCalledWith({
|
|
191
|
+
contextId: 1,
|
|
192
|
+
prompt: "Say hello",
|
|
193
|
+
params: {
|
|
194
|
+
n_predict: 16,
|
|
195
|
+
temperature: 0.2,
|
|
196
|
+
top_p: 0.8,
|
|
197
|
+
stop: ["</s>"],
|
|
198
|
+
emit_partial_completion: true,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
expect(result).toEqual({
|
|
202
|
+
text: "hello",
|
|
203
|
+
promptTokens: 3,
|
|
204
|
+
outputTokens: 2,
|
|
205
|
+
durationMs: 7,
|
|
206
|
+
});
|
|
207
|
+
expect(tokens).toEqual([["h", 1]]);
|
|
208
|
+
expect(capacitorLlama.currentModelPath()).toBe(null);
|
|
209
|
+
expect(plugin.releaseContext).toHaveBeenCalledWith({ contextId: 1 });
|
|
210
|
+
});
|
|
211
|
+
it("embeds via the native llama-cpp-capacitor embedding() method", async () => {
|
|
212
|
+
setNativeCapacitor("ios");
|
|
213
|
+
const plugin = makePluginMock();
|
|
214
|
+
mockedPlugin = plugin;
|
|
215
|
+
const { capacitorLlama } = await import("./index");
|
|
216
|
+
await capacitorLlama.load({ modelPath: "/models/test.gguf" });
|
|
217
|
+
const result = await capacitorLlama.embed({
|
|
218
|
+
input: "Embed this please",
|
|
219
|
+
embdNormalize: 2,
|
|
220
|
+
});
|
|
221
|
+
expect(plugin.embedding).toHaveBeenCalledWith({
|
|
222
|
+
contextId: 1,
|
|
223
|
+
text: "Embed this please",
|
|
224
|
+
params: { embd_normalize: 2 },
|
|
225
|
+
});
|
|
226
|
+
expect(plugin.tokenize).toHaveBeenCalledWith({
|
|
227
|
+
contextId: 1,
|
|
228
|
+
text: "Embed this please",
|
|
229
|
+
});
|
|
230
|
+
expect(result).toEqual({
|
|
231
|
+
embedding: [0.5, -0.25, 0.75],
|
|
232
|
+
tokens: 4,
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
it("throws when embed is called before load", async () => {
|
|
236
|
+
setNativeCapacitor("ios");
|
|
237
|
+
const plugin = makePluginMock();
|
|
238
|
+
mockedPlugin = plugin;
|
|
239
|
+
const { capacitorLlama } = await import("./index");
|
|
240
|
+
await expect(capacitorLlama.embed({ input: "x" })).rejects.toThrow(/No model loaded/);
|
|
241
|
+
});
|
|
242
|
+
it("registers a localInferenceLoader service without private field casts", async () => {
|
|
243
|
+
setNativeCapacitor("android");
|
|
244
|
+
const plugin = makePluginMock();
|
|
245
|
+
mockedPlugin = plugin;
|
|
246
|
+
const { registerCapacitorLlamaLoader } = await import("./index");
|
|
247
|
+
const services = new Map();
|
|
248
|
+
registerCapacitorLlamaLoader({
|
|
249
|
+
registerService(name, impl) {
|
|
250
|
+
services.set(name, impl);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
const loader = services.get("localInferenceLoader");
|
|
254
|
+
await loader.loadModel({ modelPath: "/models/mobile.gguf" });
|
|
255
|
+
expect(loader.currentModelPath()).toBe("/models/mobile.gguf");
|
|
256
|
+
await expect(loader.generate({ prompt: "Hello" })).resolves.toBe("hello");
|
|
257
|
+
await expect(loader.embed({ input: "Hi there" })).resolves.toEqual({
|
|
258
|
+
embedding: [0.5, -0.25, 0.75],
|
|
259
|
+
tokens: 4,
|
|
260
|
+
});
|
|
261
|
+
await loader.unloadModel();
|
|
262
|
+
expect(loader.currentModelPath()).toBe(null);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web fallback for `@elizaos/capacitor-llama`.
|
|
3
|
+
*
|
|
4
|
+
* On web (Vite dev server, Electrobun renderer) this package resolves to the
|
|
5
|
+
* main adapter but its `load`/`generate` methods reject with a clear
|
|
6
|
+
* "unavailable" error. The standalone node-llama-cpp engine in
|
|
7
|
+
* `@elizaos/app-core` handles desktop inference; this stub only exists so
|
|
8
|
+
* the Capacitor plugin resolution never crashes during web bundling.
|
|
9
|
+
*/
|
|
10
|
+
export type { GenerateOptions, GenerateResult, HardwareInfo, LlamaAdapter, LoadOptions, } from "./definitions";
|
|
11
|
+
export { capacitorLlama, registerCapacitorLlamaLoader } from "./index";
|
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web fallback for `@elizaos/capacitor-llama`.
|
|
3
|
+
*
|
|
4
|
+
* On web (Vite dev server, Electrobun renderer) this package resolves to the
|
|
5
|
+
* main adapter but its `load`/`generate` methods reject with a clear
|
|
6
|
+
* "unavailable" error. The standalone node-llama-cpp engine in
|
|
7
|
+
* `@elizaos/app-core` handles desktop inference; this stub only exists so
|
|
8
|
+
* the Capacitor plugin resolution never crashes during web bundling.
|
|
9
|
+
*/
|
|
10
|
+
export { capacitorLlama, registerCapacitorLlamaLoader } from "./index";
|