arcane-os 0.56.0 → 0.58.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/CHANGELOG.md +43 -0
- package/README.md +6 -2
- package/browser-runtime/ai/core-local.mjs +306 -0
- package/browser-runtime/ai/core-onnx.mjs +96 -0
- package/browser-runtime/ai/onnx-tensors.mjs +85 -0
- package/browser-runtime/core/client.mjs +151 -16
- package/docs/architecture.md +5 -2
- package/docs/platform-targets.md +30 -13
- package/docs/reference/README.md +38 -3
- package/docs/reference/availability-and-normalization.md +9 -6
- package/docs/reference/cli.md +29 -9
- package/docs/reference/core-client.md +80 -0
- package/docs/reference/core-native-packaging.md +304 -12
- package/docs/reference/core-preferences.md +49 -0
- package/docs/reference/diarization.md +206 -0
- package/docs/reference/git-text-snapshot.md +104 -0
- package/docs/reference/inventory/package-api.json +330 -22
- package/docs/reference/local-ai.md +331 -0
- package/docs/reference/protocols.md +97 -20
- package/docs/reference/pwa.md +5 -2
- package/docs/reference/runtime-modules.md +6 -5
- package/docs/reference/sdk-api.md +611 -63
- package/package.json +15 -3
- package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
- package/schemas/arcane-app.schema.json +55 -1
- package/schemas/arcane-package.schema.json +44 -0
- package/schemas/native-build-plan.schema.json +27 -1
- package/schemas/target-adapter.schema.json +2 -0
- package/src/app-descriptor.mjs +18 -2
- package/src/cli/main.mjs +20 -4
- package/src/constants.mjs +2 -0
- package/src/core/host.mjs +23 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
- package/src/core/hosts/macos/ArcaneHost.h +86 -0
- package/src/core/hosts/macos/ArcaneHost.m +455 -0
- package/src/core/hosts/macos/ArcaneLauncher.m +432 -0
- package/src/core/hosts/windows/ArcaneCoreProcess.cs +3 -3
- package/src/core/hosts/windows/ArcaneHost.cs +695 -0
- package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
- package/src/core/services/local-ai.mjs +621 -0
- package/src/core/services/packaged-web.mjs +89 -0
- package/src/core/services/preferences.mjs +201 -0
- package/src/core-classic-source.mjs +2 -2
- package/src/dev-server.mjs +9 -5
- package/src/diarization/build.mjs +35 -0
- package/src/diarization/index.mjs +432 -0
- package/src/diarization/native/CMakeLists.txt +9 -0
- package/src/diarization/native/main.cpp +269 -0
- package/src/diarization/service.mjs +129 -0
- package/src/git-text-snapshot.mjs +265 -0
- package/src/import-map.mjs +2 -0
- package/src/index.mjs +1 -0
- package/src/local-ai/archive.mjs +52 -0
- package/src/local-ai/config.mjs +62 -0
- package/src/local-ai/dev.mjs +390 -0
- package/src/local-ai/discover.mjs +71 -0
- package/src/local-ai/http.mjs +106 -0
- package/src/local-ai/install.mjs +459 -0
- package/src/local-ai/onnx-worker.mjs +85 -0
- package/src/local-ai/onnx.mjs +368 -0
- package/src/local-ai/server.mjs +262 -0
- package/src/native/macos-provider.mjs +291 -0
- package/src/native/portable-layout.mjs +37 -4
- package/src/native/portable-provider.mjs +44 -8
- package/src/native/windows-provider.mjs +262 -0
- package/src/native-plan.mjs +9 -1
- package/src/native-provider-loader.mjs +10 -2
- package/src/packager/core.mjs +6 -2
- package/src/process.mjs +184 -20
- package/src/scaffold.mjs +1 -1
- package/src/targets/index.mjs +11 -1
- package/src/templates/workspace-template.mjs +6 -5
- package/src/toolchain.mjs +33 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.58.0
|
|
4
|
+
|
|
5
|
+
- Add native ONNX sessions through `arcane-os/local-ai/onnx` and browser Core
|
|
6
|
+
access through `arcane-os/ai/core-onnx`. Explicit runtime selection uses the
|
|
7
|
+
upstream Node runtime distribution, retained per-session workers, complete
|
|
8
|
+
tensor transport, authoritative loaded state and owned cancellation/exit.
|
|
9
|
+
Apps select their graphs, feeds and processing; no model is chosen or fetched.
|
|
10
|
+
- Add native Nemotron diarization runtime preparation, helper build and Core
|
|
11
|
+
integration. Preserve complete framed input/output, independent streams,
|
|
12
|
+
cancellation and process drain. Runtime/model selection remains explicit.
|
|
13
|
+
- Add the reusable Core preferences service and Git text snapshots, preserving
|
|
14
|
+
complete caller data and existing application-owned storage and repository
|
|
15
|
+
choices. Extend raw process I/O without changing existing UTF-8 defaults.
|
|
16
|
+
- Add shared WebKit document lifecycle and macOS native host composition source,
|
|
17
|
+
portable target contracts, and explicit packaged HTTP serving through the
|
|
18
|
+
existing HTTP server. Default HTTPS and ordinary browser behavior remain.
|
|
19
|
+
macOS executable build and platform acceptance remain separately pending.
|
|
20
|
+
- No automatic runtime/model installation, application default switch or saved
|
|
21
|
+
data migration is introduced. Native host archive delivery is separate from
|
|
22
|
+
npm publication; this release does not claim an available Windows/Mac archive.
|
|
23
|
+
|
|
24
|
+
## 0.57.0
|
|
25
|
+
|
|
26
|
+
- Add local model chat through explicit llama.cpp/Ollama runtime requirements,
|
|
27
|
+
reusable installation and bundling, a retained Core local-AI service, and an
|
|
28
|
+
optional browser-to-Core provider. Preserve complete requests and streamed
|
|
29
|
+
responses, model readiness, cancellation and owned process shutdown. Apps own
|
|
30
|
+
model selection and defaults; preparation does not block ordinary page startup.
|
|
31
|
+
- Add authored Core service selection and launch context. Factories receive
|
|
32
|
+
authored options and the separate app-root/runtime context; explicit launch
|
|
33
|
+
context overrides remain supported. Portable packages can bundle explicitly
|
|
34
|
+
selected local-AI runtimes without changing browser-only output.
|
|
35
|
+
- Add Windows x64 executable packaging through the public Windows native
|
|
36
|
+
provider and the matching release's retained WebView2/Node SEA host asset.
|
|
37
|
+
Client injection precedes app scripts, service requests retain complete JSON,
|
|
38
|
+
and accepted service work drains before the owned Core process exits.
|
|
39
|
+
- Add `runProcess` cancellation mode `close-input` for owned graceful shutdown;
|
|
40
|
+
the existing `terminate-tree` default remains unchanged. Includes separately
|
|
41
|
+
delivered macOS process source, without claiming a complete macOS host.
|
|
42
|
+
- Correct the documented Node document-library subpath and lexical-search
|
|
43
|
+
example. No preferences service, Nemotron diarization, native ONNX, native
|
|
44
|
+
Kokoro or local image generation is introduced in this release.
|
|
45
|
+
|
|
3
46
|
## 0.56.0
|
|
4
47
|
|
|
5
48
|
- Add explicit `package.nativeResources` selection for native-only application
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.58.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -131,7 +131,7 @@ each device. Keep these local files ignored by Git. The same certificate pair
|
|
|
131
131
|
works for any selected app in that workspace. The command reports missing TLS
|
|
132
132
|
files instead of starting an HTTP listener.
|
|
133
133
|
|
|
134
|
-
HTTPS is the default for development and
|
|
134
|
+
HTTPS is the default for development and packaged browser previews.
|
|
135
135
|
Plain `npm run dev` binds to localhost with the same workspace certificate pair.
|
|
136
136
|
An explicit `--host` overrides the bind address, and `--port` selects the HTTPS port.
|
|
137
137
|
The paired HTTP listener returns 308 redirects and uses an OS-assigned port
|
|
@@ -152,6 +152,10 @@ This selects one HTTP content listener and prints HTTP network URLs. It reads
|
|
|
152
152
|
no certificate pair and serves the same application and generated PWA routes.
|
|
153
153
|
PWA and storage availability still depend on the browser's secure-context
|
|
154
154
|
rules; see [HTTP development](docs/reference/cli.md#explicit-http-development).
|
|
155
|
+
API callers may also explicitly select `http:true` with `mode:'packaged'` to
|
|
156
|
+
serve the selected packaged web root through the same listener owner. See
|
|
157
|
+
[`startDevServer()`](docs/reference/sdk-api.md#startdevserver) for readiness,
|
|
158
|
+
start-URL and shutdown behavior.
|
|
155
159
|
|
|
156
160
|
Open the URL printed by the server. The generated page owns its import map and
|
|
157
161
|
Arcane theme; its application module is `modules/App.js` at the repository root.
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
import {getInstalledCoreClient} from '../core/client.mjs';
|
|
3
|
+
|
|
4
|
+
const is = new Is(false);
|
|
5
|
+
const PROVIDER_PROTOCOL = 'arcane-ai-provider/2';
|
|
6
|
+
const AUTHORITY_PROTOCOL = 'arcane-ai-model-authority/1';
|
|
7
|
+
|
|
8
|
+
function localAIError(code, message, cause) {
|
|
9
|
+
const error = cause === undefined ? new Error(message) : new Error(message, {cause});
|
|
10
|
+
error.code = code;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cancelled(reason) {
|
|
15
|
+
const error = localAIError('ARCANE_AI_REQUEST_ABORTED', 'The local AI request was cancelled.', reason);
|
|
16
|
+
error.name = 'AbortError';
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Browser-safe llama.cpp access through an existing Core connection. The app
|
|
22
|
+
* selects its model; Core owns the running engine and its loaded sessions.
|
|
23
|
+
* Creating this provider neither connects a transport nor installs a runtime.
|
|
24
|
+
*/
|
|
25
|
+
export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id = 'llama.cpp'} = {}) {
|
|
26
|
+
if (!is.string(id) || !id.trim()) throw new TypeError('The Core AI provider id must be a nonempty string.');
|
|
27
|
+
if (client !== null && (!is.function(client?.invoke) || !is.function(client?.events?.on))) {
|
|
28
|
+
throw new TypeError('The Core AI client must expose invoke and events.on.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let runtime = null;
|
|
32
|
+
let models = [];
|
|
33
|
+
let selection = null;
|
|
34
|
+
let disposed = false;
|
|
35
|
+
let loading = false;
|
|
36
|
+
let sequence = 0;
|
|
37
|
+
let lifecycleRevision = 0;
|
|
38
|
+
const requests = new Set();
|
|
39
|
+
|
|
40
|
+
function requireClient() {
|
|
41
|
+
if (disposed) throw localAIError('ARCANE_AI_PROVIDER_DISPOSED', 'The Core AI provider is disposed.');
|
|
42
|
+
if (!client) throw localAIError('ARCANE_AI_CORE_UNAVAILABLE', 'Local llama.cpp requires an available Core connection.');
|
|
43
|
+
return client;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireSelection(value) {
|
|
47
|
+
if (value?.providerId !== id || !is.string(value?.modelId) || !value.modelId
|
|
48
|
+
|| value.localOnly !== true) {
|
|
49
|
+
throw localAIError('ARCANE_AI_MODEL_AUTHORITY_REQUIRED', 'The local provider requires an explicit matching provider and model.');
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadedModel() {
|
|
55
|
+
return selection && models.find(function selectedModel(model) {
|
|
56
|
+
return model.id === selection.modelId && model.loaded === true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function modelReady() {
|
|
61
|
+
return runtime?.available === true && runtime.state === 'ready' && Boolean(loadedModel());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function status() {
|
|
65
|
+
const loaded = !disposed && modelReady();
|
|
66
|
+
return {
|
|
67
|
+
state: disposed ? 'disposed' : loading ? 'loading' : loaded ? 'ready'
|
|
68
|
+
: runtime?.error ? 'error' : runtime?.available === true ? 'unloaded' : 'unavailable',
|
|
69
|
+
loaded,
|
|
70
|
+
busy: loading || requests.size > 0,
|
|
71
|
+
modelId: selection?.modelId ?? null,
|
|
72
|
+
available: !disposed && runtime?.available === true,
|
|
73
|
+
installed: runtime?.installed === true,
|
|
74
|
+
error: runtime?.error ?? null
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function acceptRuntime(value) {
|
|
79
|
+
runtime = value ?? null;
|
|
80
|
+
models = is.array(runtime?.models) ? runtime.models : [];
|
|
81
|
+
for (const request of requests) {
|
|
82
|
+
const model = models.find(function requestModel(record) { return record.id === request.modelId; });
|
|
83
|
+
if (model?.loaded === true) request.loaded = true;
|
|
84
|
+
if (runtime?.available !== true || (request.loaded && (runtime.state !== 'ready' || model?.loaded !== true))) {
|
|
85
|
+
request.controller.abort(localAIError('ARCANE_AI_MODEL_NOT_READY', 'The requested local model is no longer ready.', runtime?.error));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return status();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const unsubscribe = client?.events.on('localai.state', function localRuntimeChanged(snapshot) {
|
|
92
|
+
if (disposed || !is.array(snapshot?.runtimes)) return;
|
|
93
|
+
lifecycleRevision += 1;
|
|
94
|
+
acceptRuntime(snapshot.runtimes.find(function llamaRuntime(value) {
|
|
95
|
+
return value.id === 'llama.cpp';
|
|
96
|
+
}));
|
|
97
|
+
}) ?? function noCoreSubscription() {};
|
|
98
|
+
|
|
99
|
+
function authority(value) {
|
|
100
|
+
return {protocol: AUTHORITY_PROTOCOL, providerId: id, modelId: value.modelId};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function inspect(value, {signal} = {}) {
|
|
104
|
+
requireSelection(value);
|
|
105
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
106
|
+
try {
|
|
107
|
+
const core = requireClient();
|
|
108
|
+
const statusRevision = lifecycleRevision;
|
|
109
|
+
const current = await core.invoke('llama.status', {}, {signal});
|
|
110
|
+
if (lifecycleRevision === statusRevision) acceptRuntime(current);
|
|
111
|
+
if (runtime?.available !== true) {
|
|
112
|
+
return {
|
|
113
|
+
available: false,
|
|
114
|
+
code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
|
|
115
|
+
message: 'The local llama.cpp runtime is unavailable.',
|
|
116
|
+
error: runtime?.error ?? null
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!models.some(function matchesModel(model) { return model.id === value.modelId; })) {
|
|
120
|
+
return {
|
|
121
|
+
available: false,
|
|
122
|
+
code: 'ARCANE_AI_MODEL_UNAVAILABLE',
|
|
123
|
+
message: 'The selected model is unavailable in the local llama.cpp runtime.'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {available: true, authority: authority(value)};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (signal?.aborted || error?.name === 'AbortError') throw error;
|
|
129
|
+
acceptRuntime({...runtime, available: false, state: 'error', error});
|
|
130
|
+
return {available: false, code: error.code ?? 'ARCANE_AI_CORE_UNAVAILABLE', message: error.message, error};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function load({selection: value, signal, progress} = {}) {
|
|
135
|
+
const core = requireClient();
|
|
136
|
+
requireSelection(value);
|
|
137
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
138
|
+
selection = value;
|
|
139
|
+
loading = true;
|
|
140
|
+
try {
|
|
141
|
+
progress?.({phase: 'loading', modelId: value.modelId});
|
|
142
|
+
const revision = lifecycleRevision;
|
|
143
|
+
const current = await core.invoke('llama.load', {model: value.modelId}, {signal});
|
|
144
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
145
|
+
if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core did not report the selected model loaded and ready.', current?.error);
|
|
146
|
+
progress?.({phase: 'ready', modelId: value.modelId});
|
|
147
|
+
return {authority: authority(value), status: {...status(), state: 'ready', busy: false}};
|
|
148
|
+
} finally {
|
|
149
|
+
loading = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ownRequest(signal, modelId) {
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
function forwardAbort() { controller.abort(signal.reason); }
|
|
156
|
+
signal?.addEventListener('abort', forwardAbort, {once: true});
|
|
157
|
+
if (signal?.aborted) forwardAbort();
|
|
158
|
+
const request = {controller, result: null, modelId, loaded: models.some(function loadedTarget(model) {
|
|
159
|
+
return model.id === modelId && model.loaded === true;
|
|
160
|
+
})};
|
|
161
|
+
requests.add(request);
|
|
162
|
+
request.release = function releaseRequest() {
|
|
163
|
+
signal?.removeEventListener('abort', forwardAbort);
|
|
164
|
+
requests.delete(request);
|
|
165
|
+
};
|
|
166
|
+
return request;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function streamRequest(core, parameters, signal) {
|
|
170
|
+
const request = ownRequest(signal, parameters.model);
|
|
171
|
+
const chunks = [];
|
|
172
|
+
const readers = [];
|
|
173
|
+
let complete = false;
|
|
174
|
+
let failure = null;
|
|
175
|
+
sequence += 1;
|
|
176
|
+
const streamId = is.function(core.uuid) ? core.uuid()
|
|
177
|
+
: globalThis.crypto?.randomUUID?.() ?? `${id}:${Date.now()}:${sequence}:${Math.random()}`;
|
|
178
|
+
|
|
179
|
+
function flush() {
|
|
180
|
+
while (readers.length && chunks.length && !request.controller.signal.aborted) {
|
|
181
|
+
readers.shift().resolve({value: chunks.shift(), done: false});
|
|
182
|
+
}
|
|
183
|
+
if (!complete || (chunks.length && !request.controller.signal.aborted)) return;
|
|
184
|
+
while (readers.length) {
|
|
185
|
+
const reader = readers.shift();
|
|
186
|
+
if (failure) reader.reject(failure);
|
|
187
|
+
else reader.resolve({value: undefined, done: true});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stopChunks = core.events.on('llama.chunk', function receiveChunk(event) {
|
|
192
|
+
if (event?.streamId !== streamId || complete || request.controller.signal.aborted) return;
|
|
193
|
+
chunks.push(event.chunk);
|
|
194
|
+
flush();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
request.result = Promise.resolve().then(function invokeStream() {
|
|
198
|
+
return core.invoke('llama.chat', {...parameters, stream: true, streamId}, {signal: request.controller.signal});
|
|
199
|
+
}).then(function streamCompleted(result) {
|
|
200
|
+
if (request.controller.signal.aborted) throw cancelled(request.controller.signal.reason);
|
|
201
|
+
return result;
|
|
202
|
+
}).catch(function streamFailed(error) {
|
|
203
|
+
failure = error;
|
|
204
|
+
throw error;
|
|
205
|
+
}).finally(function releaseStream() {
|
|
206
|
+
complete = true;
|
|
207
|
+
stopChunks();
|
|
208
|
+
request.release();
|
|
209
|
+
flush();
|
|
210
|
+
});
|
|
211
|
+
request.result.catch(function observeRetainedStreamFailure() {});
|
|
212
|
+
|
|
213
|
+
function cancel(reason) {
|
|
214
|
+
if (!complete) request.controller.abort(reason);
|
|
215
|
+
return request.result.then(function cancelledAfterSuccess() {}, function cancelledAfterFailure() {});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
result: request.result,
|
|
220
|
+
cancel,
|
|
221
|
+
[Symbol.asyncIterator]() {
|
|
222
|
+
return {
|
|
223
|
+
next() {
|
|
224
|
+
if (request.controller.signal.aborted) return Promise.reject(failure ?? cancelled(request.controller.signal.reason));
|
|
225
|
+
if (chunks.length) return Promise.resolve({value: chunks.shift(), done: false});
|
|
226
|
+
if (complete) return failure ? Promise.reject(failure) : Promise.resolve({value: undefined, done: true});
|
|
227
|
+
return new Promise(function awaitChunk(resolve, reject) { readers.push({resolve, reject}); });
|
|
228
|
+
},
|
|
229
|
+
async return() {
|
|
230
|
+
await cancel();
|
|
231
|
+
return {value: undefined, done: true};
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function request({selection: value, operation, payload, signal} = {}) {
|
|
239
|
+
const core = requireClient();
|
|
240
|
+
requireSelection(value);
|
|
241
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
242
|
+
if (loading || selection?.modelId !== value.modelId) {
|
|
243
|
+
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Load the selected local model before requesting inference.');
|
|
244
|
+
}
|
|
245
|
+
const model = payload?.model ?? value.modelId;
|
|
246
|
+
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
247
|
+
const revision = lifecycleRevision;
|
|
248
|
+
const current = await core.invoke('llama.status', {}, {signal});
|
|
249
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
250
|
+
}
|
|
251
|
+
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
252
|
+
throw localAIError('ARCANE_AI_MODEL_UNAVAILABLE', 'The requested model is unavailable in the local llama.cpp runtime.');
|
|
253
|
+
}
|
|
254
|
+
if (runtime?.available !== true || runtime.state !== 'ready'
|
|
255
|
+
|| (model === value.modelId && !modelReady())) {
|
|
256
|
+
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'The requested local runtime or selected model is not ready.');
|
|
257
|
+
}
|
|
258
|
+
const parameters = {model, payload};
|
|
259
|
+
if (operation === 'stream') return streamRequest(core, parameters, signal);
|
|
260
|
+
if (operation !== 'chat') throw localAIError('ARCANE_AI_PROVIDER_OPERATION_UNAVAILABLE', 'The local llama.cpp provider supports chat and stream.');
|
|
261
|
+
const pending = ownRequest(signal, model);
|
|
262
|
+
pending.result = Promise.resolve().then(function invokeChat() {
|
|
263
|
+
return core.invoke('llama.chat', {...parameters, stream: false}, {signal: pending.controller.signal});
|
|
264
|
+
}).then(function chatCompleted(result) {
|
|
265
|
+
if (pending.controller.signal.aborted) throw cancelled(pending.controller.signal.reason);
|
|
266
|
+
return result;
|
|
267
|
+
}).finally(pending.release);
|
|
268
|
+
return pending.result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function unload({selection: value = selection, signal} = {}) {
|
|
272
|
+
const core = requireClient();
|
|
273
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
274
|
+
for (const request of requests) request.controller.abort();
|
|
275
|
+
await Promise.allSettled([...requests].map(function activeResult(request) { return request.result; }));
|
|
276
|
+
if (!value) return status();
|
|
277
|
+
requireSelection(value);
|
|
278
|
+
const revision = lifecycleRevision;
|
|
279
|
+
const current = await core.invoke('llama.unload', {model: value.modelId}, {signal});
|
|
280
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
281
|
+
selection = null;
|
|
282
|
+
return status();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function dispose(options = {}) {
|
|
286
|
+
if (disposed) return status();
|
|
287
|
+
if (selection || requests.size) await unload(options);
|
|
288
|
+
disposed = true;
|
|
289
|
+
unsubscribe();
|
|
290
|
+
return status();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
protocol: PROVIDER_PROTOCOL,
|
|
295
|
+
role: 'llm',
|
|
296
|
+
id,
|
|
297
|
+
localOnly: true,
|
|
298
|
+
catalog: function catalog() { return [...models]; },
|
|
299
|
+
status,
|
|
300
|
+
inspect,
|
|
301
|
+
load,
|
|
302
|
+
request,
|
|
303
|
+
unload,
|
|
304
|
+
dispose
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {getInstalledCoreClient} from '../core/client.mjs';
|
|
2
|
+
import {createArcaneEventSource} from '../event-manager.mjs';
|
|
3
|
+
import {encodeTensorMap, decodeTensorMap, encodeTensorFetches} from './onnx-tensors.mjs';
|
|
4
|
+
|
|
5
|
+
function unavailable(message) {
|
|
6
|
+
const error = new Error(message);
|
|
7
|
+
error.code = 'ARCANE_ONNX_CORE_UNAVAILABLE';
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Optional system ONNX access. Construction neither connects nor installs. */
|
|
12
|
+
export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {}) {
|
|
13
|
+
const owner = {};
|
|
14
|
+
const events = createArcaneEventSource(owner, {source: 'core-onnx', eventTypes: ['onnx.state']});
|
|
15
|
+
const lifetime = new AbortController();
|
|
16
|
+
const pending = new Set();
|
|
17
|
+
let state = {id: 'onnx', installed: false, available: false, state: 'unknown', models: []};
|
|
18
|
+
let revision = 0;
|
|
19
|
+
let closed = false;
|
|
20
|
+
|
|
21
|
+
function current() { return {...state, busy: pending.size > 0, closed}; }
|
|
22
|
+
function publish() { events.dispatch('onnx.state', current()); }
|
|
23
|
+
function accept(value) {
|
|
24
|
+
state = value ?? {id: 'onnx', installed: false, available: false, state: 'unavailable', models: []};
|
|
25
|
+
revision += 1;
|
|
26
|
+
publish();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const unsubscribe = client?.events.on('localai.state', function localStateChanged(snapshot) {
|
|
30
|
+
if (!closed) accept(snapshot?.runtimes?.find(function onnxRuntime(runtime) { return runtime.id === 'onnx'; }));
|
|
31
|
+
}) ?? function noCoreSubscription() {};
|
|
32
|
+
|
|
33
|
+
async function invoke(method, parameters, {signal, timeoutMs = 0} = {}) {
|
|
34
|
+
if (closed) throw unavailable('The Core ONNX accessor is closed.');
|
|
35
|
+
if (!client) throw unavailable('Local ONNX requires an available Core connection.');
|
|
36
|
+
const request = {};
|
|
37
|
+
const operationSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
38
|
+
operationSignal.throwIfAborted();
|
|
39
|
+
pending.add(request);
|
|
40
|
+
publish();
|
|
41
|
+
try {
|
|
42
|
+
return await client.invoke(method, parameters, {signal: operationSignal, timeoutMs});
|
|
43
|
+
} finally {
|
|
44
|
+
pending.delete(request);
|
|
45
|
+
if (!closed) publish();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function inspect(options) {
|
|
50
|
+
const before = revision;
|
|
51
|
+
try {
|
|
52
|
+
const result = await invoke('onnx.status', {}, options);
|
|
53
|
+
if (!closed && revision === before) accept(result);
|
|
54
|
+
return current();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!closed && !options?.signal?.aborted && revision === before) {
|
|
57
|
+
accept({...state, available: false, state: 'unavailable', error});
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function load({id, model, sessionOptions, signal, timeoutMs} = {}) {
|
|
64
|
+
return invoke('onnx.load', {id, model, sessionOptions}, {signal, timeoutMs});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function run({id, feeds, fetches, runOptions, signal, timeoutMs} = {}) {
|
|
68
|
+
const result = await invoke('onnx.run', {
|
|
69
|
+
id, feeds: encodeTensorMap(feeds),
|
|
70
|
+
fetches: encodeTensorFetches(fetches), runOptions
|
|
71
|
+
}, {signal, timeoutMs});
|
|
72
|
+
return decodeTensorMap(result);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unload({id, signal, timeoutMs} = {}) {
|
|
76
|
+
return invoke('onnx.unload', {id}, {signal, timeoutMs});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function subscribe(listener) {
|
|
80
|
+
const stop = events.on('onnx.state', function changed(event) { listener(event.detail); });
|
|
81
|
+
try { listener(current()); } catch (error) { stop(); throw error; }
|
|
82
|
+
return stop;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function close() {
|
|
86
|
+
if (closed) return;
|
|
87
|
+
closed = true;
|
|
88
|
+
lifetime.abort();
|
|
89
|
+
unsubscribe();
|
|
90
|
+
state = {...state, available: false, state: 'closed'};
|
|
91
|
+
publish();
|
|
92
|
+
events.dispose();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {load, run, unload, inspect, current, subscribe, close};
|
|
96
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
|
|
3
|
+
const is = new Is(false);
|
|
4
|
+
const typedArrays = {
|
|
5
|
+
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
|
|
6
|
+
Int32Array, Uint32Array, Float32Array, Float64Array,
|
|
7
|
+
BigInt64Array, BigUint64Array, Float16Array: globalThis.Float16Array
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Tensor framing belongs at JSON transport boundaries, outside inference. */
|
|
11
|
+
export function encodeTensorMap(tensors) {
|
|
12
|
+
if (tensors === undefined || tensors === null) return tensors;
|
|
13
|
+
return Object.fromEntries(Object.entries(tensors).map(function encodeEntry([name, tensor]) {
|
|
14
|
+
return [name, tensor === null ? null : {
|
|
15
|
+
type: tensor.type, dims: tensor.dims, data: encodeTensorData(tensor.data)
|
|
16
|
+
}];
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function decodeTensorMap(tensors) {
|
|
21
|
+
if (tensors === undefined || tensors === null) return tensors;
|
|
22
|
+
return Object.fromEntries(Object.entries(tensors).map(function decodeEntry([name, tensor]) {
|
|
23
|
+
return [name, tensor === null ? null : {
|
|
24
|
+
type: tensor.type, dims: tensor.dims, data: decodeTensorData(tensor.data)
|
|
25
|
+
}];
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function encodeTensorFetches(fetches) {
|
|
30
|
+
return is.array(fetches) ? fetches : encodeTensorMap(fetches);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decodeTensorFetches(fetches) {
|
|
34
|
+
return is.array(fetches) ? fetches : decodeTensorMap(fetches);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function encodeTensorData(data) {
|
|
38
|
+
if (is.typedArray(data)) {
|
|
39
|
+
const arrayType = Object.prototype.toString.call(data).slice(8, -1);
|
|
40
|
+
const content = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
41
|
+
return {encoding: 'base64', arrayType, content: encodeBase64(content)};
|
|
42
|
+
}
|
|
43
|
+
if (is.array(data)) return data.map(encodeScalar);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function decodeTensorData(data) {
|
|
48
|
+
if (data?.encoding === 'base64') {
|
|
49
|
+
const Constructor = typedArrays[data.arrayType];
|
|
50
|
+
if (!is.function(Constructor)) {
|
|
51
|
+
throw new TypeError(`Tensor data representation ${data.arrayType} is unavailable in this runtime.`);
|
|
52
|
+
}
|
|
53
|
+
const content = decodeBase64(data.content);
|
|
54
|
+
return new Constructor(content.buffer);
|
|
55
|
+
}
|
|
56
|
+
if (is.array(data)) return data.map(decodeScalar);
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function encodeScalar(value) {
|
|
61
|
+
if (is.bigint(value)) return {encoding: 'bigint', value: String(value)};
|
|
62
|
+
if (is.negativeZero(value)) return {encoding: 'number', value: '-0'};
|
|
63
|
+
if (is.number(value) && !is.finite(value)) return {encoding: 'number', value: String(value)};
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function decodeScalar(value) {
|
|
68
|
+
if (value?.encoding === 'bigint') return BigInt(value.value);
|
|
69
|
+
if (value?.encoding === 'number') return Number(value.value);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function encodeBase64(content) {
|
|
74
|
+
if (is.function(content.toBase64)) return content.toBase64();
|
|
75
|
+
let binary = '';
|
|
76
|
+
for (const value of content) binary += String.fromCharCode(value);
|
|
77
|
+
return btoa(binary);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function decodeBase64(content) {
|
|
81
|
+
if (is.function(Uint8Array.fromBase64)) return Uint8Array.fromBase64(content);
|
|
82
|
+
return Uint8Array.from(atob(content), function characterCode(character) {
|
|
83
|
+
return character.charCodeAt(0);
|
|
84
|
+
});
|
|
85
|
+
}
|